Skip to content

Commit

Permalink
Merge spesmilo#28: Name script support
Browse files Browse the repository at this point in the history
fef9da4 Namecoin: properly handle malformed script in get_name_op_from_output_script (JeremyRand)
f5332b6 Namecoin: properly handle None argument to split_name_script (JeremyRand)
901be4b Namecoin: Add missing name_op field in tests (JeremyRand)
4f83182 Namecoin: Set default TxOutput name_op to None (JeremyRand)
7697804 Namecoin: Add name_show console command (JeremyRand)
f430b8c Namecoin: Format ASCII values (JeremyRand)
1e32ab2 Namecoin: Handle id/ names in identifier formatting (JeremyRand)
4c9a25a Namecoin: Display formatted name identifiers (JeremyRand)
8de0105 Namecoin: Fix name scripts with empty values (JeremyRand)
18e43bd Namecoin: Fix name_new transactions being recognized as transfers (JeremyRand)
d774840 Namecoin: Fix tuple size mismatch in get_wallet_delta (JeremyRand)
0dac8a1 Namecoin: Generate default label for name transfers. (JeremyRand)
eb2f962 Namecoin: Generate default label for name transactions. (JeremyRand)
d060326 Namecoin: Deserialize name scripts. (JeremyRand)
5fc9e66 Namecoin: name script support in transaction.get_address_from_output_script (JeremyRand)

Pull request description:
  • Loading branch information
JeremyRand committed Oct 1, 2018
2 parents 3c920cb + fef9da4 commit eeca38d
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 22 deletions.
2 changes: 1 addition & 1 deletion electrum_nmc/address_synchronizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ def get_wallet_delta(self, tx):
is_partial = True
if not is_mine:
is_partial = False
for addr, value in tx.get_outputs():
for addr, value, name_op in tx.get_outputs():
v_out += value
if self.is_mine(addr):
v_out_mine += value
Expand Down
2 changes: 1 addition & 1 deletion electrum_nmc/auxpow.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ def fast_txid(tx):
def stub_parse_output(vds, i):
vds.read_int64() # d['value']
vds.read_bytes(vds.read_compact_size()) # scriptPubKey
return {'type': TYPE_SCRIPT, 'address': None, 'value': 0}
return {'type': TYPE_SCRIPT, 'address': None, 'value': 0, 'name_op': None}

# This is equivalent to tx.deserialize(), but doesn't parse outputs.
def fast_tx_deserialize(tx):
Expand Down
76 changes: 76 additions & 0 deletions electrum_nmc/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from . import bitcoin
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
from .i18n import _
from .names import name_identifier_to_scripthash
from .transaction import Transaction, multisig_script, TxOutput
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
from .plugin import run_hook
Expand Down Expand Up @@ -676,6 +677,81 @@ def getfeerate(self, fee_method=None, fee_level=None):
fee_level = Decimal(fee_level)
return self.config.fee_per_kb(dyn=dyn, mempool=mempool, fee_level=fee_level)

@command('wn')
def name_show(self, identifier):
# 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))

# Pick the most recent name op that's [12, 36000) confirmations.
chain_height = self.network.blockchain().height()
safe_height_max = chain_height - 12
safe_height_min = chain_height - 35999

tx_best = None
for tx_candidate in txs[::-1]:
if tx_candidate["height"] <= safe_height_max and tx_candidate["height"] >= safe_height_min:
tx_best = tx_candidate
break
if tx_best is None:
raise Exception("Invalid height")
txid = tx_best["tx_hash"]
height = tx_best["height"]

# The height is now verified to be safe.

# TODO: This will write data to the wallet, which may be a privacy
# leak. We should allow a null wallet to be used.
self.network.run_from_another_thread(self.wallet.verifier._request_and_verify_single_proof(txid, height))

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

if self.wallet and txid in self.wallet.transactions:
tx = self.wallet.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")

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

for idx, o in enumerate(tx.outputs()):
if o.name_op is not None:
if "name" in o.name_op:
if o.name_op["name"] != identifier_bytes:
# Identifier mismatch. This will definitely fail under
# current Namecoin consensus rules, but in a future
# hardfork there might be multiple name outputs, so we
# might as well future-proof and scan the other
# outputs.
continue

# the tx is now verified to represent the identifier at a
# safe height in the blockchain

return {
"name": o.name_op["name"].decode("ascii"),
"name_encoding": "ascii",
"value": o.name_op["value"].decode("ascii"),
"value_encoding": "ascii",
"txid": txid,
"vout": idx,
"address": o.address,
"height": height,
"expires_in": height - chain_height + 36000,
"expired": False,
"ismine": self.wallet.is_mine(o.address),
}

raise Exception("missing name op")

@command('')
def help(self):
# for the python console
Expand Down
6 changes: 5 additions & 1 deletion electrum_nmc/gui/qt/transaction_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from electrum_nmc.bitcoin import base_encode
from electrum_nmc.i18n import _
from electrum_nmc.names import format_name_op
from electrum_nmc.plugin import run_hook
from electrum_nmc import simple_config
from electrum_nmc.util import bfh
Expand Down Expand Up @@ -319,11 +320,14 @@ def format_amount(amt):
o_text.setFont(QFont(MONOSPACE_FONT))
o_text.setReadOnly(True)
cursor = o_text.textCursor()
for addr, v in self.tx.get_outputs():
for addr, v, name_op in self.tx.get_outputs():
cursor.insertText(addr, text_format(addr))
if v is not None:
cursor.insertText('\t', ext)
cursor.insertText(format_amount(v), ext)
if name_op is not None:
cursor.insertText('\n', ext)
cursor.insertText(format_name_op(name_op), ext)
cursor.insertBlock()
vbox.addWidget(o_text)

Expand Down
266 changes: 266 additions & 0 deletions electrum_nmc/names.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
#!/usr/bin/env python
#
# Electrum-NMC - lightweight Namecoin client
# Copyright (C) 2018 Namecoin Developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

def split_name_script(decoded):
# This case happens if a script was malformed and couldn't be decoded by
# transaction.get_address_from_output_script.
if decoded is None:
return {"name_op": None, "address_scriptPubKey": decoded}

# So, Namecoin Core uses OP_0 when pushing an empty string as a (value).
# Unfortunately, Electrum doesn't match OP_0 when using OP_PUSHDATA4 as a
# data push opcode wildcard. So we have to check for OP_0 separately,
# otherwise we'll fail to detect name operations with an empty (value).
# Technically, we should be doing the same check for the (name), but I
# can't be bothered to make the code more complex just to help out whoever
# registered the empty string. The (hash) and (rand) are constant-length
# (at least in practice; not sure about consensus rules), so they're
# unaffected.

# name_new TxOuts look like:
# NAME_NEW (hash) 2DROP (Bitcoin TxOut)
match = [ OP_NAME_NEW, opcodes.OP_PUSHDATA4, opcodes.OP_2DROP ]
if match_decoded(decoded[:len(match)], match):
return {"name_op": {"op": OP_NAME_NEW, "hash": decoded[1][1]}, "address_scriptPubKey": decoded[len(match):]}

# name_firstupdate TxOuts look like:
# NAME_FIRSTUPDATE (name) (rand) (value) 2DROP 2DROP (Bitcoin TxOut)
match = [ OP_NAME_FIRSTUPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_2DROP, opcodes.OP_2DROP ]
match_empty_value = [ OP_NAME_FIRSTUPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_0, opcodes.OP_2DROP, opcodes.OP_2DROP ]
if match_decoded(decoded[:len(match)], match) or match_decoded(decoded[:len(match_empty_value)], match_empty_value):
return {"name_op": {"op": OP_NAME_FIRSTUPDATE, "name": decoded[1][1], "rand": decoded[2][1], "value": decoded[3][1]}, "address_scriptPubKey": decoded[len(match):]}

# name_update TxOuts look like:
# NAME_UPDATE (name) (value) 2DROP DROP (Bitcoin TxOut)
match = [ OP_NAME_UPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_PUSHDATA4, opcodes.OP_2DROP, opcodes.OP_DROP ]
match_empty_value = [ OP_NAME_UPDATE, opcodes.OP_PUSHDATA4, opcodes.OP_0, opcodes.OP_2DROP, opcodes.OP_DROP ]
if match_decoded(decoded[:len(match)], match) or match_decoded(decoded[:len(match_empty_value)], match_empty_value):
return {"name_op": {"op": OP_NAME_UPDATE, "name": decoded[1][1], "value": decoded[2][1]}, "address_scriptPubKey": decoded[len(match):]}

return {"name_op": None, "address_scriptPubKey": decoded}

def get_name_op_from_output_script(_bytes):
try:
decoded = [x for x in script_GetOp(_bytes)]
except MalformedBitcoinScript:
decoded = None

# Extract the name script if one is present.
return split_name_script(decoded)["name_op"]

def name_op_to_script(name_op):
if name_op is None:
script = ''
elif name_op["op"] == OP_NAME_NEW:
script = '51' # OP_NAME_NEW
script += push_script(bh2u(name_op["hash"]))
script += '6d' # OP_2DROP
elif name_op["op"] == OP_NAME_FIRSTUPDATE:
script = '52' # OP_NAME_FIRSTUPDATE
script += push_script(bh2u(name_op["name"]))
script += push_script(bh2u(name_op["rand"]))
script += push_script(bh2u(name_op["value"]))
script += '6d' # OP_2DROP
script += '6d' # OP_2DROP
elif name_op["op"] == OP_NAME_UPDATE:
script = '53' # OP_NAME_UPDATE
script += push_script(bh2u(name_op["name"]))
script += push_script(bh2u(name_op["value"]))
script += '6d' # OP_2DROP
script += '75' # OP_DROP
else:
raise BitcoinException('unknown name op: {}'.format(name_op))
return script


def name_identifier_to_scripthash(identifier_bytes):
name_op = {"op": OP_NAME_UPDATE, "name": identifier_bytes, "value": bytes([])}
script = name_op_to_script(name_op)
script += '6a' # OP_RETURN

return script_to_scripthash(script)


def format_name_identifier(identifier_bytes):
try:
identifier = identifier_bytes.decode("ascii")
except UnicodeDecodeError:
return format_name_identifier_unknown_hex(identifier_bytes)

is_domain_namespace = identifier.startswith("d/")
if is_domain_namespace:
return format_name_identifier_domain(identifier)

is_identity_namespace = identifier.startswith("id/")
if is_identity_namespace:
return format_name_identifier_identity(identifier)

return format_name_identifier_unknown(identifier)


def format_name_identifier_domain(identifier):
label = identifier[len("d/"):]

if len(label) < 1:
return format_name_identifier_unknown(identifier)

# Source: https://github.com/namecoin/proposals/blob/master/ifa-0001.md#keys
if len(label) > 63:
return format_name_identifier_unknown(identifier)

# Source: https://github.com/namecoin/proposals/blob/master/ifa-0001.md#keys
label_regex = r"^(xn--)?[a-z0-9]+(-[a-z0-9]+)*$"
label_match = re.match(label_regex, label)
if label_match is None:
return format_name_identifier_unknown(identifier)

# Reject digits-only labels
number_regex = r"^[0-9]+$"
number_match = re.match(number_regex, label)
if number_match is not None:
return format_name_identifier_unknown(identifier)

return "Domain " + label + ".bit"


def format_name_identifier_identity(identifier):
label = identifier[len("id/"):]

if len(label) < 1:
return format_name_identifier_unknown(identifier)

# Max id/ identifier length is 255 chars according to wiki spec. But we
# don't need to check for this, because that's also the max length of an
# identifier under the Namecoin consensus rules.

# Same as d/ regex but without IDN prefix.
# TODO: this doesn't exactly match the https://wiki.namecoin.org spec.
label_regex = r"^[a-z0-9]+(-[a-z0-9]+)*$"
label_match = re.match(label_regex, label)
if label_match is None:
return format_name_identifier_unknown(identifier)

return "Identity " + label

def format_name_identifier_unknown(identifier):
# Check for non-printable characters, and print ASCII if none are found.
if identifier.isprintable():
return 'Non-standard name "' + identifier + '"'

return format_name_identifier_unknown_hex(identifier.encode("ascii"))


def format_name_identifier_unknown_hex(identifier_bytes):
return "Non-standard hex name " + bh2u(identifier_bytes)


def format_name_value(identifier_bytes):
try:
identifier = identifier_bytes.decode("ascii")
except UnicodeDecodeError:
return format_name_value_hex(identifier_bytes)

if not identifier.isprintable():
return format_name_value_hex(identifier_bytes)

return "ASCII " + identifier


def format_name_value_hex(identifier_bytes):
return "Hex " + bh2u(identifier_bytes)


def format_name_op(name_op):
if name_op is None:
return ''
if "hash" in name_op:
formatted_hash = "Commitment = " + bh2u(name_op["hash"])
if "rand" in name_op:
formatted_rand = "Salt = " + bh2u(name_op["rand"])
if "name" in name_op:
formatted_name = "Name = " + format_name_identifier(name_op["name"])
if "value" in name_op:
formatted_value = "Data = " + format_name_value(name_op["value"])

if name_op["op"] == OP_NAME_NEW:
return "\tPre-Registration\n\t\t" + formatted_hash
if name_op["op"] == OP_NAME_FIRSTUPDATE:
return "\tRegistration\n\t\t" + formatted_name + "\n\t\t" + formatted_rand + "\n\t\t" + formatted_value
if name_op["op"] == OP_NAME_UPDATE:
return "\tUpdate\n\t\t" + formatted_name + "\n\t\t" + formatted_value


def get_default_name_tx_label(wallet, tx):
for addr, v, name_op in tx.get_outputs():
if name_op is not None:
# TODO: Handle multiple atomic name ops.
name_input_is_mine, name_output_is_mine = get_wallet_name_delta(wallet, tx)
if not name_input_is_mine and not name_output_is_mine:
return None
if name_input_is_mine and not name_output_is_mine:
return "Transfer (Outgoing): " + format_name_identifier(name_op["name"])
if not name_input_is_mine and name_output_is_mine:
# A name_new transaction isn't expected to have a name input,
# so we don't consider it a transfer.
if name_op["op"] != OP_NAME_NEW:
return "Transfer (Incoming): " + format_name_identifier(name_op["name"])
if name_op["op"] == OP_NAME_NEW:
# A name_new transaction doesn't have a name output, so there's
# nothing to format.
return "Pre-Registration"
if name_op["op"] == OP_NAME_FIRSTUPDATE:
return "Registration: " + format_name_identifier(name_op["name"])
if name_op["op"] == OP_NAME_UPDATE:
return "Update: " + format_name_identifier(name_op["name"])
return None


def get_wallet_name_delta(wallet, tx):
name_input_is_mine = False
name_output_is_mine = False
for txin in tx.inputs():
addr = wallet.get_txin_address(txin)
if wallet.is_mine(addr):
prev_tx = wallet.transactions.get(txin['prevout_hash'])
if prev_tx.get_outputs()[txin['prevout_n']][2] is not None:
name_input_is_mine = True
for addr, value, name_op in tx.get_outputs():
if name_op is not None and wallet.is_mine(addr):
name_output_is_mine = True

return name_input_is_mine, name_output_is_mine


import binascii
import re

from .bitcoin import push_script, script_to_scripthash
from .transaction import MalformedBitcoinScript, match_decoded, opcodes, script_GetOp
from .util import bh2u

OP_NAME_NEW = opcodes.OP_1
OP_NAME_FIRSTUPDATE = opcodes.OP_2
OP_NAME_UPDATE = opcodes.OP_3

Loading

0 comments on commit eeca38d

Please sign in to comment.