Skip to content

feat(sighash): verify required SighashAll [part 7/7] #917

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

Open
wants to merge 1 commit into
base: refactor/verification/scripts
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions hathor/transaction/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ class TooManySighashSubsets(TxValidationError):
"""More sighash subsets than the configured maximum."""


class MissingSighashAll(TxValidationError):
"""At least one input is required to be signed using SighashAll."""


class InvalidOutputValue(TxValidationError):
"""Value of output is invalid"""

Expand Down
8 changes: 5 additions & 3 deletions hathor/verification/transaction_verifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
InvalidInputData,
InvalidInputDataSize,
InvalidToken,
MissingSighashAll,
NoInputError,
OutputNotSelected,
RewardLocked,
Expand All @@ -39,7 +40,7 @@
)
from hathor.transaction.scripts import script_eval
from hathor.transaction.scripts.script_context import ScriptContext
from hathor.transaction.scripts.sighash import get_unique_sighash_subsets
from hathor.transaction.scripts.sighash import SighashAll, get_unique_sighash_subsets
from hathor.transaction.transaction import TokenInfo
from hathor.transaction.util import get_deposit_amount, get_withdraw_amount
from hathor.types import TokenUid, VertexId
Expand Down Expand Up @@ -144,7 +145,6 @@ def verify_scripts(self, tx: Transaction, *, spent_txs: dict[VertexId, BaseTrans
script contexts.
"""
all_contexts: list[ScriptContext] = []

for input_index, input_tx in enumerate(tx.inputs):
try:
script_context = script_eval(tx, input_tx, spent_txs[input_tx.tx_id], input_index=input_index)
Expand All @@ -155,7 +155,6 @@ def verify_scripts(self, tx: Transaction, *, spent_txs: dict[VertexId, BaseTrans

all_sighashes = [context.get_sighash() for context in all_contexts]
sighash_subsets = get_unique_sighash_subsets(all_sighashes)

all_max_sighash_subsets = [context.get_max_sighash_subsets() for context in all_contexts]
valid_max_sighash_subsets: list[int] = [
max_subsets for max_subsets in all_max_sighash_subsets if max_subsets is not None
Expand All @@ -175,6 +174,9 @@ def verify_scripts(self, tx: Transaction, *, spent_txs: dict[VertexId, BaseTrans
if index not in all_selected_outputs:
raise OutputNotSelected(f'Output at index {index} is not signed by any input.')

if not any(isinstance(sighash, SighashAll) for sighash in all_sighashes):
raise MissingSighashAll('At least one input is required to be signed using SighashAll.')

def verify_reward_locked(self, tx: Transaction) -> None:
"""Will raise `RewardLocked` if any reward is spent before the best block height is enough, considering both
the block rewards spent by this tx itself, and the inherited `min_height`."""
Expand Down
20 changes: 15 additions & 5 deletions tests/tx/scripts/test_sighash_bitmask.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from hathor.exception import InvalidNewTransaction
from hathor.manager import HathorManager
from hathor.transaction import Transaction, TxInput, TxOutput
from hathor.transaction.exceptions import InputOutputMismatch, InvalidInputData, InvalidScriptError
from hathor.transaction.exceptions import InputOutputMismatch, InvalidInputData, InvalidScriptError, MissingSighashAll
from hathor.transaction.scripts.p2pkh import P2PKH
from hathor.transaction.scripts.sighash import InputsOutputsLimit, SighashBitmask
from hathor.util import not_none
Expand Down Expand Up @@ -93,9 +93,14 @@ def test_sighash_bitmask(self) -> None:
sighash=sighash_bitmask,
)

# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx)
# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs, and they're
# missing a SighashAll
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx, skip_script=True)

with pytest.raises(InputOutputMismatch):
self.manager1.verification_service.verifiers.tx.verify_sum(atomic_swap_tx)

with pytest.raises(MissingSighashAll):
self.manager1.verification_service.verify(atomic_swap_tx)

# Alice sends the tx bytes to Bob, represented here by cloning the tx
Expand Down Expand Up @@ -166,9 +171,14 @@ def test_sighash_bitmask_with_limit(self) -> None:
inputs_outputs_limit=InputsOutputsLimit(max_inputs=2, max_outputs=3)
)

# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx)
# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs, and they're
# missing a SighashAll
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx, skip_script=True)

with pytest.raises(InputOutputMismatch):
self.manager1.verification_service.verifiers.tx.verify_sum(atomic_swap_tx)

with pytest.raises(MissingSighashAll):
self.manager1.verification_service.verify(atomic_swap_tx)

# Alice sends the tx bytes to Bob, represented here by cloning the tx
Expand Down
37 changes: 29 additions & 8 deletions tests/tx/scripts/test_sighash_range.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
InputOutputMismatch,
InvalidInputData,
InvalidScriptError,
MissingSighashAll,
TooManySighashSubsets,
)
from hathor.transaction.scripts import MultiSig
Expand Down Expand Up @@ -100,9 +101,14 @@ def test_sighash_range(self) -> None:
sighash=sighash_range,
)

# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx)
# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs, and they're
# missing a SighashAll
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx, skip_script=True)

with pytest.raises(InputOutputMismatch):
self.manager1.verification_service.verifiers.tx.verify_sum(atomic_swap_tx)

with pytest.raises(MissingSighashAll):
self.manager1.verification_service.verify(atomic_swap_tx)

# Alice sends the tx bytes to Bob, represented here by cloning the tx
Expand Down Expand Up @@ -195,9 +201,14 @@ def test_sighash_range_with_multisig(self) -> None:
sighash=sighash_range,
)

# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx)
# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs, and they're
# missing a SighashAll
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx, skip_script=True)

with pytest.raises(InputOutputMismatch):
self.manager1.verification_service.verifiers.tx.verify_sum(atomic_swap_tx)

with pytest.raises(MissingSighashAll):
self.manager1.verification_service.verify(atomic_swap_tx)

# Alice sends the tx bytes to Bob, represented here by cloning the tx
Expand Down Expand Up @@ -268,9 +279,14 @@ def test_sighash_range_with_limit(self) -> None:
inputs_outputs_limit=InputsOutputsLimit(max_inputs=2, max_outputs=3)
)

# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx)
# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs, and they're
# missing a SighashAll
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx, skip_script=True)

with pytest.raises(InputOutputMismatch):
self.manager1.verification_service.verifiers.tx.verify_sum(atomic_swap_tx)

with pytest.raises(MissingSighashAll):
self.manager1.verification_service.verify(atomic_swap_tx)

# Alice sends the tx bytes to Bob, represented here by cloning the tx
Expand Down Expand Up @@ -450,9 +466,14 @@ def test_sighash_range_with_max_subsets(self) -> None:
max_sighash_subsets=1,
)

# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx)
# At this point, the tx is partial. The inputs are valid, but they're mismatched with outputs, and they're
# missing a SighashAll
self.manager1.verification_service.verifiers.tx.verify_inputs(atomic_swap_tx, skip_script=True)

with pytest.raises(InputOutputMismatch):
self.manager1.verification_service.verifiers.tx.verify_sum(atomic_swap_tx)

with pytest.raises(MissingSighashAll):
self.manager1.verification_service.verify(atomic_swap_tx)

# Alice sends the tx bytes to Bob, represented here by cloning the tx
Expand Down
32 changes: 32 additions & 0 deletions tests/tx/test_tx.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
InvalidInputDataSize,
InvalidOutputScriptSize,
InvalidOutputValue,
MissingSighashAll,
NoInputError,
OutputNotSelected,
ParentDoesNotExist,
Expand Down Expand Up @@ -216,6 +217,37 @@ def test_too_many_sighash_subsets(self) -> None:

self.assertEqual(str(e.value), "There are more custom sighash subsets than the configured maximum (1 > 0).")

@patch('hathor.transaction.scripts.opcode.is_opcode_valid', lambda _: True)
def test_missing_sighash_all(self) -> None:
parents = [tx.hash for tx in self.genesis_txs]
genesis_block = self.genesis_blocks[0]

value = genesis_block.outputs[0].value
address = get_address_from_public_key(self.genesis_public_key)
script = P2PKH.create_output_script(address)
output = TxOutput(value, script)

tx_input = TxInput(genesis_block.hash, 0, b'')
tx = Transaction(
weight=1,
inputs=[tx_input],
outputs=[output],
parents=parents,
storage=self.tx_storage,
timestamp=self.last_block.timestamp + 1
)

sighash = SighashBitmask(inputs=0b1, outputs=0b1)
data_to_sign = tx.get_custom_sighash_data(sighash)
public_bytes, signature = self.wallet.get_input_aux_data(data_to_sign, self.genesis_private_key)
tx_input.data = P2PKH.create_input_data(public_bytes, signature, sighash=sighash)

self.manager.cpu_mining_service.resolve(tx)
with pytest.raises(MissingSighashAll) as e:
self.manager.verification_service.verify(tx)

self.assertEqual(str(e.value), 'At least one input is required to be signed using SighashAll.')

def _gen_tx_spending_genesis_block(self):
parents = [tx.hash for tx in self.genesis_txs]
genesis_block = self.genesis_blocks[0]
Expand Down
Loading