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
10 changes: 4 additions & 6 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ See [checkcontractverify.md](checkcontractverify.md) for the semantics of `OP_CH

## Contracts, programs and clauses

The internal pubkey (or the _naked_ pubkey for an augmented P2TR), together with the taptree, constitutes the ___program___ of the contract, which encodes all the spending conditions of the contract.
The internal pubkey (the _naked_ pubkey), together with the taptree, constitutes the ___program___ of the contract, which encodes all the spending conditions of the contract.

All contracts in this framework are _augmented_ P2TR contracts, meaning they can have embedded data. For stateless contracts (those without state), the embedded data is simply the empty buffer `b''`, which by the semantics of `tweak_embed_data` leaves the internal pubkey unchanged. Note that embedding the empty buffer b'' (or the number 0, which encodes as the empty buffer in Script) leaves the internal pubkey unchanged per tweak_embed_data semantics, and can therefore be used for any contract that does not actually need to embed any data.

An actual UTXO whose `scriptPubKey` is a program, possibly with some specified embedded _data_, is a ___contract instance___.

We call ___clause___ each of the spending conditions in the taptree of. Each clause might also specify the state transition rules, by defining the program of one or more of the outputs.<br>
We call ___clause___ each of the spending conditions in the taptree. Each clause might also specify the state transition rules, by defining the program of one or more of the outputs.<br>
The keypath, if not a NUMS (Nothing-Up-My-Sleeve) point, can also be considered an additional special clause with no condition on the outputs.

### Merklelized data
Expand Down Expand Up @@ -109,10 +111,6 @@ The spending condition can be any predicate that can be expressed in Script, wit

_Note_: this ignores the technical details of how to encode/decode the state variables to/from a single hash; that is an implementation detail that can safely be left out when discussing the semantic of a smart contract.

### Default contract

The contract `P2TR{pk}` is equal to the output script descriptor `tr(pk)`.

### Example: Vault

With the above conventions, we can model the Vault contract drawn above as follows:
Expand Down
4 changes: 2 additions & 2 deletions docs/matt.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ A script that adds such restriction is called a covenant, and that is not possib
The core idea in MATT is to introduce the following capability to be accessible within Script:

- force an output to have a certain Script (and their amounts)
- attach a piece of data to an output
- optionally, attach a piece of data to an output
- read the data of the current input (or another one)

The first is common to many other covenant proposals, for example [OP_CHECKTEMPLATEVERIFY](https://github.com/bitcoin/bips/blob/master/bip-0119.mediawiki) is a long-discussed proposal that can constrain all the outputs at the same time.

The part relative to the data is more specific: this data can be as short as a 32-byte hash, but the key is that the data of an output is not decided when the UTXO is first created, but it is dynamically computed in Script (and therefore it can depend on "parameters" that are passed by the spender). This is extremely powerful, as it allows to create some sort of "state machines" where the execution can decide:
The part relative to the data is more specific: this data can be an arbitrary buffer. The key is that the data of an output is not decided when the UTXO is first created, but it is dynamically computed in Script (and therefore it can depend on "parameters" that are passed by the spender). This is extremely powerful, as it allows to create some sort of "state machines" where the execution can decide:

- what is the next "state" of the state machine (by constraining the Script of the outputs)
- what is the "data" attached to the next state
Expand Down
6 changes: 4 additions & 2 deletions examples/game256/game256_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from matt.argtypes import BytesType, IntType, SignerType
from matt.btctools.common import sha256
from matt.btctools.script import OP_ADD, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_FROMALTSTACK, OP_NOT, OP_PICK, OP_ROT, OP_SHA256, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, CScript
from matt.contracts import ClauseOutput, StandardClause, StandardAugmentedP2TR, StandardP2TR, ContractState
from matt.contracts import ClauseOutput, StandardClause, StandardAugmentedP2TR, ContractState
from matt.hub.fraud import Bisect_1, Computer, Leaf
from matt.merkle import MerkleTree
from matt.script_helpers import check_input_contract, check_output_contract, dup, merkle_root, older
Expand All @@ -19,7 +19,9 @@
# Do we need "clause" algebra?


class G256_S0(StandardP2TR):
class G256_S0(StandardAugmentedP2TR):
State = None # Stateless contract

def __init__(self, alice_pk: bytes, bob_pk: bytes, forfait_timeout: int = 10):
self.alice_pk = alice_pk
self.bob_pk = bob_pk
Expand Down
10 changes: 6 additions & 4 deletions examples/rps/rps_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from matt.btctools.messages import sha256
from matt.btctools import script
from matt.btctools.script import OP_ADD, OP_CAT, OP_CHECKSIG, OP_CHECKTEMPLATEVERIFY, OP_DUP, OP_ENDIF, OP_EQUALVERIFY, OP_FROMALTSTACK, OP_IF, OP_LESSTHAN, OP_OVER, OP_SHA256, OP_SUB, OP_SWAP, OP_TOALTSTACK, OP_VERIFY, OP_WITHIN, CScript
from matt.contracts import P2TR, ClauseOutput, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState
from matt.contracts import ClauseOutput, SimpleP2TR, StandardClause, StandardAugmentedP2TR, ContractState
from matt.script_helpers import check_input_contract, check_output_contract
from matt.utils import encode_wit_element, make_ctv_template

Expand Down Expand Up @@ -49,7 +49,9 @@ def calculate_hash(move: int, r: bytes) -> bytes:
# - c_a
# spending conditions:
# - bob_pk (m_b) => RPSGameS1[m_b]
class RPSGameS0(StandardP2TR):
class RPSGameS0(StandardAugmentedP2TR):
State = None # Stateless contract

def __init__(self, alice_pk: bytes, bob_pk: bytes, c_a: bytes, stake: int = DEFAULT_STAKE):
assert len(alice_pk) == 32 and len(bob_pk) == 32 and len(c_a) == 32

Expand Down Expand Up @@ -160,8 +162,8 @@ def make_script(diff: int, ctv_hash: bytes):
OP_CHECKTEMPLATEVERIFY
])

alice_spk = P2TR(self.alice_pk, []).get_tr_info().scriptPubKey
bob_spk = P2TR(self.bob_pk, []).get_tr_info().scriptPubKey
alice_spk = SimpleP2TR(self.alice_pk).get_tr_info(b'').scriptPubKey
bob_spk = SimpleP2TR(self.bob_pk).get_tr_info(b'').scriptPubKey

tmpl_alice_wins = make_ctv_template([(alice_spk, 2*self.stake)])
tmpl_bob_wins = make_ctv_template([(bob_spk, 2*self.stake)])
Expand Down
6 changes: 4 additions & 2 deletions examples/vault/minivault_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from matt import CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, NUMS_KEY
from matt.argtypes import BytesType, IntType, SignerType
from matt.btctools.script import OP_CHECKCONTRACTVERIFY, OP_CHECKSIG, OP_DUP, OP_PICK, OP_SWAP, OP_TRUE, CScript
from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState
from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardAugmentedP2TR, ContractState
from matt.script_helpers import check_input_contract, older


class Vault(StandardP2TR):
class Vault(StandardAugmentedP2TR):
State = None # Stateless contract

def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes, unvault_pk: bytes, *, has_partial_revault=True, has_early_recover=True):
assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 and len(unvault_pk) == 32

Expand Down
6 changes: 4 additions & 2 deletions examples/vault/vault_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
from matt import CCV_FLAG_DEDUCT_OUTPUT_AMOUNT, NUMS_KEY
from matt.argtypes import BytesType, IntType, SignerType
from matt.btctools.script import OP_CHECKCONTRACTVERIFY, OP_CHECKSIG, OP_CHECKTEMPLATEVERIFY, OP_DUP, OP_SWAP, OP_TRUE, CScript
from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardP2TR, StandardAugmentedP2TR, ContractState
from matt.contracts import ClauseOutput, ClauseOutputAmountBehaviour, OpaqueP2TR, StandardClause, StandardAugmentedP2TR, ContractState
from matt.script_helpers import check_input_contract, older


class Vault(StandardP2TR):
class Vault(StandardAugmentedP2TR):
State = None # Stateless contract

def __init__(self, alternate_pk: Optional[bytes], spend_delay: int, recover_pk: bytes, unvault_pk: bytes, *, has_partial_revault=True, has_early_recover=True):
assert (alternate_pk is None or len(alternate_pk) == 32) and len(recover_pk) == 32 and len(unvault_pk) == 32

Expand Down
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ max-line-length = 120

[tool.pytest.ini_options]
python_files = '*.py'
pythonpath = [ 'src' ]
testpaths = [ 'tests' ]


Expand Down
127 changes: 58 additions & 69 deletions src/matt/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ def encoder_script(*args, **kwargs) -> CScript:
pass


@dataclass
class EmptyState(ContractState):
"""
Represents the empty state for stateless contracts.
When used, the data tweak is empty (b''), which by tweak_embed_data semantics leaves the key unchanged.
"""

def encode(self) -> bytes:
return b''

@staticmethod
def encoder_script() -> CScript:
return CScript([])


# Singleton instance for stateless contracts
EMPTY_STATE = EmptyState()


class ClauseOutputAmountBehaviour(Enum):
"""
Defines the semantic of a clause with respect to an output's amount, where the output is enforced by OP_CHECKCONTRACTVERIFY.
Expand All @@ -74,21 +93,21 @@ class ClauseOutput:
contract instance.

This class encapsulates the details necessary to construct an output from a contract clause, including
the output index, the contract of that output, the internal state of the output (or None if the output is
not augmented), and the amount semantic for that output.
the output index, the contract of that output, the internal state of the output, and the amount semantic
for that output.

Attributes:
n (Optional[int]): The index of the output. A value of -1 implies that the output's index equals
the current input's index.
next_contract (AbstractContract): The contract of this output.
next_state (Optional[ContractState]): The state data for the next contract instance. This is
compulsory for augmented contracts, and it must be None otherwise.
next_state (ContractState): The state data for the next contract instance. Defaults to EMPTY_STATE
for stateless contracts.
next_amount (ClauseOutputAmountBehaviour): Determines the semantic of the output amount.
"""

n: int
next_contract: AbstractContract # only StandardP2TR and StandardAugmentedP2TR are supported so far
next_state: Optional[ContractState] = None # only meaningful if the contract is augmented
next_contract: AbstractContract
next_state: 'ContractState' = EMPTY_STATE
next_amount: ClauseOutputAmountBehaviour = ClauseOutputAmountBehaviour.PRESERVE_OUTPUT

def __repr__(self):
Expand Down Expand Up @@ -126,14 +145,14 @@ def stack_elements_from_args(self, args: dict) -> List[bytes]:
pass

@abstractmethod
def next_outputs(self, args: dict, state: Union[ContractState, None] = None) -> List[ClauseOutput]:
def next_outputs(self, args: dict, state: ContractState = EMPTY_STATE) -> List[ClauseOutput]:
"""
Determines the resulting outputs of the clause, based on the arguments of the clause,
and the contract state.

Parameters:
args (Dict): The arguments of the claise.
state (Union[ContractState, None], optional): The current state of the contract, if applicable.
state (ContractState): The current state of the contract. Defaults to EMPTY_STATE for stateless contracts.

Returns:
List[ClauseOutput]: The outputs generated by the clause.
Expand Down Expand Up @@ -178,22 +197,22 @@ def __init__(self, name: str, script: CScript, arg_specs: List[Tuple[str, ArgTyp
script (CScript): The Script associated with the clause.
arg_specs (List[Tuple[str, ArgType]]): Specifications for the clause's arguments.
next_outputs_fn (Optional[Callable]): An optional function to compute the next outputs based on
the clause's arguments and the current contract state. The function can eiter return a list
of clause outputs, or a CTV template.
the clause's arguments and the current contract state (defaults to EMPTY_STATE for stateless).
The function can either return a list of clause outputs, or a CTV template.
"""

super().__init__(name, script)
self.arg_specs = arg_specs

self.next_outputs_fn = next_outputs_fn

def next_outputs(self, args: dict, state: Optional[ContractState]) -> Union[List[ClauseOutput], CTransaction]:
def next_outputs(self, args: dict, state: ContractState = EMPTY_STATE) -> Union[List[ClauseOutput], CTransaction]:
"""
Computes the outputs produced by the clause, based on the clause arguments and the contract state.

Parameters:
args (dict): The arguments to the clause.
state (Optional[ContractState]): The current state of the contract, if relevant.
state (ContractState): The current state of the contract. Defaults to EMPTY_STATE for stateless contracts.

Returns:
Union[List[ClauseOutput], CTransaction]: The outputs generated by the clause, or a CTV template.
Expand Down Expand Up @@ -290,28 +309,6 @@ def __repr__(self) -> str:
TaptreeDescription = List['TaptreeDescription']


class P2TR(AbstractContract):
"""
A class representing a Pay-to-Taproot script.
"""

def __init__(self, internal_pubkey: bytes, scripts: TaptreeDescription):
assert len(internal_pubkey) == 32

self.internal_pubkey = internal_pubkey
self.scripts = scripts
self.tr_info = script.taproot_construct(internal_pubkey, scripts)

def get_tr_info(self) -> TaprootInfo:
return self.tr_info

def get_address(self) -> str:
return encode_segwit_address("bcrt", 1, bytes(self.get_tr_info().scriptPubKey)[2:])

def __repr__(self):
return f"{self.__class__.__name__}(internal_pubkey={self.internal_pubkey.hex()})"


class AugmentedP2TR(AbstractContract):
"""
An abstract class representing a Pay-to-Taproot script with some embedded data.
Expand Down Expand Up @@ -340,6 +337,28 @@ def __repr__(self):
return f"{self.__class__.__name__}(naked_internal_pubkey={self.naked_internal_pubkey.hex()}. Contracts's data: {self.data})"


class SimpleP2TR(AugmentedP2TR):
"""
A simple Pay-to-Taproot contract with an optional taptree.
Useful for creating basic P2TR outputs without defining a full contract class.
"""

def __init__(self, pubkey: bytes, taptree: TaptreeDescription = []):
"""
Initializes a SimpleP2TR contract.

Parameters:
pubkey (bytes): The 32-byte public key.
taptree (TaptreeDescription): The taptree description. Defaults to [] for keypath-only spend.
"""
assert len(pubkey) == 32
super().__init__(pubkey)
self.taptree = taptree

def get_scripts(self) -> TaptreeDescription:
return self.taptree


StandardTaptreeDescription = Union[StandardClause, List['StandardTaptreeDescription']]


Expand All @@ -365,40 +384,15 @@ def _flatten_standard_taptree_description(std_tree: StandardTaptreeDescription)
return [std_tree]


class StandardP2TR(P2TR):
class StandardAugmentedP2TR(AugmentedP2TR, ABC):
"""
A StandardP2TR where all the transitions are given by a StandardClause.
An AugmentedP2TR where all the transitions are given by a StandardClause.
"""

def __init__(self, internal_pubkey: bytes, standard_taptree: StandardTaptreeDescription):
super().__init__(internal_pubkey, _normalize_standard_taptree_description(standard_taptree))
self.standard_taptree = standard_taptree
self.clauses = _flatten_standard_taptree_description(standard_taptree)
self._clauses_dict = {clause.name: clause for clause in self.clauses}

def get_scripts(self) -> List[Tuple[str, CScript]]:
return list(map(lambda clause: (clause.name, clause.script), self.clauses))

def decode_wit_stack(self, stack_elems: List[bytes]) -> Tuple[str, dict]:
leaf_hash = stack_elems[-2]

clause_name = None
for clause in self.clauses:
if leaf_hash == self.get_tr_info().leaves[clause.name].script:
clause_name = clause.name
break
if clause_name is None:
raise ValueError("Clause not found")

return clause_name, self._clauses_dict[clause_name].args_from_stack_elements(stack_elems[:-2])

def __repr__(self):
return f"{self.__class__.__name__}(internal_pubkey={self.internal_pubkey.hex()})"


class StandardAugmentedP2TR(AugmentedP2TR, ABC):
State: Optional[Type[ContractState]] = None
"""
An AugmentedP2TR where all the transitions are given by a StandardClause.
The State class for this contract, or None for stateless contracts.
Subclasses with state should override this with their ContractState subclass.
"""

def __init__(self, naked_internal_pubkey: bytes, standard_taptree: StandardTaptreeDescription):
Expand All @@ -425,8 +419,3 @@ def decode_wit_stack(self, data: bytes, stack_elems: List[bytes]) -> Tuple[str,

def __repr__(self):
return f"{self.__class__.__name__}(naked_internal_pubkey={self.naked_internal_pubkey.hex()})"

@property
@abstractmethod
def State() -> Type[ContractState]:
pass
Loading
Loading