Skip to content

Commit

Permalink
Fix Emercoin nvs values parse (#899)
Browse files Browse the repository at this point in the history
* Fix nvs values parse for Emercoin

* Add tests
  • Loading branch information
yakimka authored and Neil committed Sep 26, 2019
1 parent 6c15149 commit d306727
Show file tree
Hide file tree
Showing 3 changed files with 241 additions and 68 deletions.
159 changes: 94 additions & 65 deletions electrumx/lib/coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,37 +344,7 @@ class BitcoinMixin(object):


class NameMixin(object):

@staticmethod
def find_end_position_of_name(script, length):
"""Finds the end position of the name data
Given the number of opcodes in the name prefix (length), returns the
index into the byte array of where the name prefix ends."""
n = 0
for _i in range(length):
# Content of this loop is copied from Script.get_ops's loop
op = script[n]
n += 1

if op <= OpCodes.OP_PUSHDATA4:
# Raw bytes follow
if op < OpCodes.OP_PUSHDATA1:
dlen = op
elif op == OpCodes.OP_PUSHDATA1:
dlen = script[n]
n += 1
elif op == OpCodes.OP_PUSHDATA2:
dlen, = struct.unpack('<H', script[n: n + 2])
n += 2
else:
dlen, = struct.unpack('<I', script[n: n + 4])
n += 4
if n + dlen > len(script):
raise IndexError
n += dlen

return n
DATA_PUSH_MULTIPLE = -2

@classmethod
def interpret_name_prefix(cls, script, possible_ops):
Expand All @@ -386,8 +356,9 @@ def interpret_name_prefix(cls, script, possible_ops):
possible_ops must be an array of arrays, defining the structures
of name prefixes to look out for. Each array can consist of
actual opcodes, -1 for ignored data placeholders and strings for
named placeholders. Whenever a data push matches a named placeholder,
actual opcodes, -1 for ignored data placeholders, -2 for
multiple ignored data placeholders and strings for named placeholders.
Whenever a data push matches a named placeholder,
the corresponding value is put into a dictionary the placeholder name
as key, and the dictionary of matches is returned."""

Expand All @@ -398,18 +369,25 @@ def interpret_name_prefix(cls, script, possible_ops):

name_op_count = None
for pops in possible_ops:
n = len(pops)

# Start by translating named placeholders to -1 values, and
# keeping track of which op they corresponded to.
template = []
named_index = {}
for i in range(n):
if type(pops[i]) == str:

n = len(pops)
offset = 0
for i, op in enumerate(pops):
if op == cls.DATA_PUSH_MULTIPLE:
# Emercoin stores value in multiple placeholders
# Script structure: https://git.io/fjuRu
added, template = cls._add_data_placeholders_to_template(ops[i:], template)
offset += added - 1 # subtract the "DATA_PUSH_MULTIPLE" opcode
elif type(op) == str:
template.append(-1)
named_index[pops[i]] = i
named_index[op] = i + offset
else:
template.append(pops[i])
template.append(op)
n += offset

if not _match_ops(ops[:n], template):
continue
Expand All @@ -426,6 +404,64 @@ def interpret_name_prefix(cls, script, possible_ops):
address_script = script[name_end_pos:]
return named_values, address_script

@classmethod
def _add_data_placeholders_to_template(cls, opcodes, template):
num_dp = cls._read_data_placeholders_count(opcodes)
num_2drop = num_dp // 2
num_drop = num_dp % 2

two_drops = [OpCodes.OP_2DROP for _ in range(num_2drop)]
one_drops = [OpCodes.OP_DROP for _ in range(num_drop)]

elements_added = num_dp + num_2drop + num_drop
placeholders = [-1 for _ in range(num_dp)]
drops = two_drops + one_drops

return elements_added, template + placeholders + drops

@classmethod
def _read_data_placeholders_count(cls, opcodes):
data_placeholders = 0

for opcode in opcodes:
if type(opcode) == tuple:
data_placeholders += 1
else:
break

return data_placeholders

@staticmethod
def find_end_position_of_name(script, length):
"""Finds the end position of the name data
Given the number of opcodes in the name prefix (length), returns the
index into the byte array of where the name prefix ends."""
n = 0
for _i in range(length):
# Content of this loop is copied from Script.get_ops's loop
op = script[n]
n += 1

if op <= OpCodes.OP_PUSHDATA4:
# Raw bytes follow
if op < OpCodes.OP_PUSHDATA1:
dlen = op
elif op == OpCodes.OP_PUSHDATA1:
dlen = script[n]
n += 1
elif op == OpCodes.OP_PUSHDATA2:
dlen, = struct.unpack('<H', script[n: n + 2])
n += 2
else:
dlen, = struct.unpack('<I', script[n: n + 4])
n += 4
if n + dlen > len(script):
raise IndexError
n += dlen

return n


class NameIndexMixin(NameMixin):
"""Shared definitions for coins that have a name index
Expand Down Expand Up @@ -672,6 +708,24 @@ class Emercoin(NameMixin, Coin):

PEERS = []

# Name opcodes
OP_NAME_NEW = OpCodes.OP_1
OP_NAME_UPDATE = OpCodes.OP_2
OP_NAME_DELETE = OpCodes.OP_3

# Valid name prefixes.
NAME_NEW_OPS = [OP_NAME_NEW, OpCodes.OP_DROP, "name", "days",
OpCodes.OP_2DROP, NameMixin.DATA_PUSH_MULTIPLE]
NAME_UPDATE_OPS = [OP_NAME_UPDATE, OpCodes.OP_DROP, "name", "days",
OpCodes.OP_2DROP, NameMixin.DATA_PUSH_MULTIPLE]
NAME_DELETE_OPS = [OP_NAME_DELETE, OpCodes.OP_DROP, "name",
OpCodes.OP_DROP]
NAME_OPERATIONS = [
NAME_NEW_OPS,
NAME_UPDATE_OPS,
NAME_DELETE_OPS,
]

@classmethod
def block_header(cls, block, height):
'''Returns the block header given a block and its height.'''
Expand All @@ -688,35 +742,10 @@ def header_hash(cls, header):

@classmethod
def hashX_from_script(cls, script):
address_script = cls.address_script_from_script(script)
_, address_script = cls.interpret_name_prefix(script, cls.NAME_OPERATIONS)

return super().hashX_from_script(address_script)

@classmethod
def address_script_from_script(cls, script):
# Name opcodes
OP_NAME_NEW = OpCodes.OP_1
OP_NAME_UPDATE = OpCodes.OP_2
OP_NAME_DELETE = OpCodes.OP_3

# Opcode sequences for name operations
# Script structure: https://git.io/fjuRu
NAME_NEW_OPS = [OP_NAME_NEW, OpCodes.OP_DROP, -1, -1,
OpCodes.OP_2DROP, -1, OpCodes.OP_DROP]
NAME_UPDATE_OPS = [OP_NAME_UPDATE, OpCodes.OP_DROP, -1, -1,
OpCodes.OP_2DROP, -1, OpCodes.OP_DROP]
NAME_DELETE_OPS = [OP_NAME_DELETE, OpCodes.OP_DROP, -1,
OpCodes.OP_DROP]

ops = [
NAME_NEW_OPS,
NAME_UPDATE_OPS,
NAME_DELETE_OPS,
]

_, address_script = cls.interpret_name_prefix(script, ops)
return address_script


class BitcoinTestnetMixin(object):
SHORTNAME = "XTN"
Expand Down
98 changes: 95 additions & 3 deletions tests/lib/test_coins.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import pytest

from electrumx.lib.coins import BitcoinSV
from electrumx.lib.script import OpCodes

from electrumx.lib.coins import BitcoinSV, NameMixin
from electrumx.lib.script import OpCodes, Script

coin = BitcoinSV


@pytest.mark.parametrize("script", (
bytes([OpCodes.OP_RETURN]),
bytes([OpCodes.OP_RETURN]) + bytes([2, 28, 50]),
Expand All @@ -27,3 +27,95 @@ def test_op_return(script):
))
def test_not_op_return(script):
assert coin.hashX_from_script(script) is not None


NAME = "name".encode("ascii")
DAYS = hex(6).encode("ascii")
VALUE = "value".encode("ascii")
ADDRESS_SCRIPT = "address_script".encode("ascii")

OP_NAME_NEW = OpCodes.OP_1
OP_NAME_UPDATE = OpCodes.OP_2
OP_DROP = OpCodes.OP_DROP
OP_2DROP = OpCodes.OP_2DROP
DP_MULT = NameMixin.DATA_PUSH_MULTIPLE


def create_script(pattern, address_script):
script = bytearray()
for item in pattern:
if type(item) == int:
script.append(item)
else:
script.extend(Script.push_data(item))
script.extend(address_script)

return bytes(script)


@pytest.mark.parametrize("opcode,pattern", (
([OP_NAME_NEW, OP_DROP, -1, -1, OP_2DROP, -1, OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, OP_DROP]),
([OP_NAME_NEW, OP_DROP, -1, -1, OP_2DROP, DP_MULT],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, OP_DROP]),
([OP_NAME_NEW, OP_DROP, -1, -1, OP_2DROP, DP_MULT],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, VALUE, OP_2DROP]),
([OP_NAME_NEW, OP_DROP, -1, OP_2DROP, DP_MULT, -1, OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, OP_2DROP, VALUE, OP_DROP, DAYS, OP_DROP]),
([OP_NAME_NEW, OP_DROP, -1, OP_2DROP, DP_MULT, -1, OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, OP_2DROP, VALUE, VALUE, OP_2DROP, DAYS, OP_DROP]),
))
def test_name_mixin_interpret_name_prefix(opcode, pattern):
ops = [opcode]
script = create_script(pattern, ADDRESS_SCRIPT)
parsed_names, parsed_address_script = NameMixin.interpret_name_prefix(script, ops)

assert len(parsed_names) == 0
assert parsed_address_script == ADDRESS_SCRIPT


@pytest.mark.parametrize("opcode,pattern", (
([OP_NAME_NEW, OP_DROP, "name", "days", OP_2DROP, -1, OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, OP_DROP]),
([OP_NAME_NEW, OP_DROP, "name", OP_DROP, -1, OP_DROP, "days", OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, OP_DROP, VALUE, OP_DROP, DAYS, OP_DROP]),
([OP_NAME_NEW, OP_DROP, "name", "days", OP_2DROP, DP_MULT],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, OP_DROP]),
([OP_NAME_NEW, OP_DROP, "name", "days", OP_2DROP, DP_MULT],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, VALUE, OP_2DROP]),
([OP_NAME_NEW, OP_DROP, "name", "days", OP_2DROP, DP_MULT],
[OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP, VALUE, VALUE, VALUE, OP_2DROP, OP_DROP]),
([OP_NAME_NEW, OP_DROP, "name", OP_2DROP, DP_MULT, "days", OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, OP_2DROP, VALUE, OP_DROP, DAYS, OP_DROP]),
([OP_NAME_NEW, OP_DROP, "name", OP_2DROP, DP_MULT, "days", OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, OP_2DROP, VALUE, VALUE, OP_2DROP, DAYS, OP_DROP]),
([OP_NAME_NEW, OP_DROP, "name", OP_2DROP, DP_MULT, "days", OP_DROP],
[OP_NAME_NEW, OP_DROP, NAME, OP_2DROP, VALUE, VALUE, VALUE, OP_2DROP, OP_DROP, DAYS, OP_DROP]),
))
def test_name_mixin_interpret_name_prefix_with_named_placeholders(opcode, pattern):
ops = [opcode]
script = create_script(pattern, ADDRESS_SCRIPT)
parsed_names, parsed_address_script = NameMixin.interpret_name_prefix(script, ops)

assert parsed_names["name"][1] == NAME
assert parsed_names["days"][1] == DAYS
assert parsed_address_script == ADDRESS_SCRIPT


@pytest.mark.parametrize("opcode", (
[OP_NAME_UPDATE, OP_DROP, -1, -1, OP_2DROP, -1, OP_DROP],
[OP_NAME_NEW, OP_DROP, -1, -1, OP_DROP, OP_DROP, -1, OP_DROP],
[OP_NAME_NEW, OP_DROP, "name", "days", OP_DROP, -1, OP_DROP],
))
def test_name_mixin_interpret_name_prefix_wrong_ops(opcode):
ops = [opcode]
script = create_script([OP_NAME_NEW, OP_DROP, NAME, DAYS, OP_2DROP,
VALUE, OP_DROP], ADDRESS_SCRIPT)
parsed_names, parsed_address_script = NameMixin.interpret_name_prefix(script, ops)

assert parsed_names is None
assert parsed_address_script == script
Loading

0 comments on commit d306727

Please sign in to comment.