Skip to content

Commit

Permalink
Merge pull request #388 from SCMusson/feat_cert_script
Browse files Browse the repository at this point in the history
Methods to add certificate scripts for smart staking
  • Loading branch information
nielstron authored Oct 22, 2024
2 parents cdddf75 + c95fb61 commit a2a9014
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
5901e9010000323232323232323232323232322232323232323232323232323374a90001bb1498c8c8ccccd400c01001401840084004030030488888c8c8c94ccd5cd19180d88009980c180aa8012400c2930992999ab9a32301c100133019301650034801052615333573464603820029404c00452613263357389201136e6f7420612076616c696420707572706f7365004988c00852624984004c0454004405840584c98cd5ce2481104e616d654572726f723a207e626f6f6c004984c98cd5ce2481144e616d654572726f723a2076616c696461746f72004984c98cd5ce2481124e616d654572726f723a20707572706f7365004984c98cd5ce2481104e616d654572726f723a20646174756d004984c98cd5ce2481124e616d654572726f723a20636f6e74657874004984c98cd5ce2481144e616d654572726f723a20526577617264696e67004984c98cd5ce2481154e616d654572726f723a2043657274696679696e67004980080088c010c0200048c0140040108c018c94ccd55cf800899319ab9c49010a496e6465784572726f72004984d5d1000800919000a80091aab9d3754002e1c8d55cf1baa00123253335573e002264c66ae712410a496e6465784572726f72004984d5d0800800919ba548018cd5d028009bb14988cdd2a400866ae814004dd8a4c1
2 changes: 1 addition & 1 deletion integration-test/test/test_certificate.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def test_stake_delegation(self):
builder = TransactionBuilder(self.chain_context)

builder.add_input_address(giver_address)
builder.add_output(TransactionOutput(address, 440000000000))
builder.add_output(TransactionOutput(address, 44000000000))

signed_tx = builder.build_and_sign([self.payment_skey], giver_address)

Expand Down
97 changes: 97 additions & 0 deletions integration-test/test/test_certificate_script.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import os
import time

import cbor2
from retry import retry

from pycardano import *

from .base import TEST_RETRIES, TestBase


class TestDelegation(TestBase):
@retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4))
def test_stake_delegation(self):
with open("./plutus_scripts/pass_certifying_and_rewarding.plutus", "r") as f:
script_hex = f.read()
stake_script = PlutusV2Script(bytes.fromhex(script_hex))
cert_script_hash = plutus_script_hash(stake_script)
address = Address(
self.payment_key_pair.verification_key.hash(),
cert_script_hash,
self.NETWORK,
)

utxos = self.chain_context.utxos(address)

if not utxos:
giver_address = Address(self.payment_vkey.hash(), network=self.NETWORK)

builder = TransactionBuilder(self.chain_context)

builder.add_input_address(giver_address)
builder.add_output(TransactionOutput(address, 44000000000))

signed_tx = builder.build_and_sign([self.payment_skey], giver_address)

print("############### Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor_hex())
print("############### Submitting transaction ###############")
self.chain_context.submit_tx(signed_tx)

time.sleep(3)

stake_credential = StakeCredential(cert_script_hash)
stake_registration = StakeRegistration(stake_credential)
pool_hash = PoolKeyHash(bytes.fromhex(os.environ.get("POOL_ID").strip()))
stake_delegation = StakeDelegation(stake_credential, pool_keyhash=pool_hash)

builder = TransactionBuilder(self.chain_context)

builder.add_input_address(address)
builder.add_output(TransactionOutput(address, 35000000))
builder.certificates = [stake_registration, stake_delegation]
redeemer = Redeemer(0)
builder.add_certificate_script(stake_script, redeemer=redeemer)

signed_tx = builder.build_and_sign(
[self.payment_key_pair.signing_key],
address,
)

print("############### Transaction created ###############")
print(signed_tx)
print(signed_tx.to_cbor_hex())
print("############### Submitting transaction ###############")
self.chain_context.submit_tx(signed_tx)


# time.sleep(8)
#
# builder = TransactionBuilder(self.chain_context)
#
# builder.add_input_address(address)
#
# stake_address = Address(
# staking_part=cert_script_hash,
# network=self.NETWORK,
# )
#
# builder.withdrawals = Withdrawals({bytes(stake_address): 0})
#
# builder.add_output(TransactionOutput(address, 1000000))
# redeemer = Redeemer(0)
# builder.add_withdrawal_script(stake_script, redeemer=redeemer)
#
# signed_tx = builder.build_and_sign(
# [self.payment_key_pair.signing_key],
# address,
# )
#
# print("############### Transaction created ###############")
# print(signed_tx)
# print(signed_tx.to_cbor_hex())
# print("############### Submitting transaction ###############")
# self.chain_context.submit_tx(signed_tx)
#
2 changes: 1 addition & 1 deletion pycardano/plutus.py
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ class RedeemerTag(CBORSerializable, Enum):

SPEND = 0
MINT = 1
CERT = 2
CERTIFICATE = 2
WITHDRAWAL = 3

def to_primitive(self) -> int:
Expand Down
53 changes: 53 additions & 0 deletions pycardano/txbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ class TransactionBuilder:
field(init=False, default_factory=lambda: [])
)

_certificate_script_to_redeemers: List[Tuple[ScriptType, Optional[Redeemer]]] = (
field(init=False, default_factory=lambda: [])
)

_inputs_to_scripts: Dict[UTxO, ScriptType] = field(
init=False, default_factory=lambda: {}
)
Expand Down Expand Up @@ -384,6 +388,49 @@ def add_withdrawal_script(
self._withdrawal_script_to_redeemers.append((script, redeemer))
return self

def add_certificate_script(
self,
script: Union[
UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script
],
redeemer: Optional[Redeemer] = None,
) -> TransactionBuilder:
"""Add a certificate script along with its redeemer to this transaction.
WARNING: The order of operations matters.
The index of the redeemer will be set to the index of the last certificate added.
Args:
script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script.
redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO.
Returns:
TransactionBuilder: Current transaction builder.
"""
if redeemer:
if redeemer.tag is not None and redeemer.tag != RedeemerTag.CERTIFICATE:
raise InvalidArgumentException(
f"Expect the redeemer tag's type to be {RedeemerTag.CERTIFICATE}, "
f"but got {redeemer.tag} instead."
)
assert self.certificates is not None and len(self.certificates) >= 1, (
"self.certificates is None. redeemer.index needs to be set to the index of the corresponding"
"certificate (defaulting to the last certificate) however no certificates could be found"
)
redeemer.index = len(self.certificates) - 1
redeemer.tag = RedeemerTag.CERTIFICATE
self._consolidate_redeemer(redeemer)

if isinstance(script, UTxO):
assert script.output.script is not None
self._certificate_script_to_redeemers.append(
(script.output.script, redeemer)
)
self.reference_inputs.add(script)
self._reference_scripts.append(script.output.script)
else:
self._certificate_script_to_redeemers.append((script, redeemer))
return self

def add_input_address(self, address: Union[Address, str]) -> TransactionBuilder:
"""Add an address to transaction's input address.
Unlike :meth:`add_input`, which deterministically adds a UTxO to the transaction's inputs, `add_input_address`
Expand Down Expand Up @@ -472,6 +519,9 @@ def all_scripts(self) -> List[ScriptType]:
for s, _ in self._withdrawal_script_to_redeemers:
scripts[script_hash(s)] = s

for s, _ in self._certificate_script_to_redeemers:
scripts[script_hash(s)] = s

return list(scripts.values())

@property
Expand All @@ -497,6 +547,7 @@ def _redeemer_list(self) -> List[Redeemer]:
[r for r in self._inputs_to_redeemers.values() if r is not None]
+ [r for _, r in self._minting_script_to_redeemers if r is not None]
+ [r for _, r in self._withdrawal_script_to_redeemers if r is not None]
+ [r for _, r in self._certificate_script_to_redeemers if r is not None]
)

def redeemers(self) -> Redeemers:
Expand Down Expand Up @@ -879,6 +930,8 @@ def _dfs(script: NativeScript):
def _set_redeemer_index(self):
# Set redeemers' index according to section 4.1 in
# https://hydra.iohk.io/build/13099856/download/1/alonzo-changes.pdf
#
# There is no way to determine certificate index here

if self.mint:
sorted_mint_policies = sorted(self.mint.keys(), key=lambda x: x.to_cbor())
Expand Down
82 changes: 82 additions & 0 deletions test/pycardano/test_txbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1363,6 +1363,88 @@ def test_tx_builder_certificates(chain_context):
assert expected == tx_body.to_primitive()


def test_tx_builder_certificates_script(chain_context):
tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])])
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
sender_address = Address.from_primitive(sender)

plutus_script = PlutusV2Script(b"dummy test script")
script_hash = plutus_script_hash(plutus_script)

stake_credential = StakeCredential(script_hash)

pool_hash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE)

stake_registration = StakeRegistration(stake_credential)

stake_delegation = StakeDelegation(stake_credential, pool_hash)

# Add sender address as input
tx_builder.add_input_address(sender).add_output(
TransactionOutput.from_primitive([sender, 500000])
)

tx_builder.certificates = [stake_registration, stake_delegation]
redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000))
tx_builder.add_certificate_script(plutus_script, redeemer=redeemer)
tx_builder.ttl = 123456

tx_builder.build(change_address=sender_address)
tx_builder.use_redeemer_map = False
witness = tx_builder.build_witness_set()
assert [redeemer] == witness.redeemer
assert witness.redeemer[0].index == 1
assert [plutus_script] == witness.plutus_v2_script


def test_tx_builder_cert_redeemer_wrong_tag(chain_context):
tx_builder = TransactionBuilder(chain_context)
plutus_script = PlutusV2Script(b"dummy test script")
redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000))
redeemer.tag = RedeemerTag.MINT
with pytest.raises(InvalidArgumentException) as e:
tx_builder.add_certificate_script(plutus_script, redeemer=redeemer)


def test_add_cert_script_from_utxo(chain_context):
tx_builder = TransactionBuilder(chain_context)
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
sender_address = Address.from_primitive(sender)
plutus_script = PlutusV2Script(b"dummy test script")
script_hash = plutus_script_hash(plutus_script)
script_address = Address(script_hash)
existing_script_utxo = UTxO(
TransactionInput.from_primitive(
[
"41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7",
1,
]
),
TransactionOutput(script_address, 1234567, script=plutus_script),
)

stake_credential = StakeCredential(script_hash)
pool_hash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE)
stake_registration = StakeRegistration(stake_credential)
stake_delegation = StakeDelegation(stake_credential, pool_hash)
tx_builder.certificates = [stake_registration, stake_delegation]
tx_builder.add_input_address(sender).add_output(
TransactionOutput.from_primitive([sender, 500000])
)

redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000))
tx_builder.add_certificate_script(existing_script_utxo, redeemer=redeemer)
tx_builder.ttl = 123456

tx_body = tx_builder.build(change_address=sender_address)
tx_builder.use_redeemer_map = False
witness = tx_builder.build_witness_set()
assert witness.plutus_data is None
assert [redeemer] == witness.redeemer
assert witness.plutus_v2_script is None
assert [existing_script_utxo.input] == tx_body.reference_inputs


def test_tx_builder_stake_pool_registration(chain_context, pool_params):
tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])])
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
Expand Down

0 comments on commit a2a9014

Please sign in to comment.