Skip to content

Commit a2a9014

Browse files
authored
Merge pull request #388 from SCMusson/feat_cert_script
Methods to add certificate scripts for smart staking
2 parents cdddf75 + c95fb61 commit a2a9014

File tree

6 files changed

+235
-2
lines changed

6 files changed

+235
-2
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
5901e9010000323232323232323232323232322232323232323232323232323374a90001bb1498c8c8ccccd400c01001401840084004030030488888c8c8c94ccd5cd19180d88009980c180aa8012400c2930992999ab9a32301c100133019301650034801052615333573464603820029404c00452613263357389201136e6f7420612076616c696420707572706f7365004988c00852624984004c0454004405840584c98cd5ce2481104e616d654572726f723a207e626f6f6c004984c98cd5ce2481144e616d654572726f723a2076616c696461746f72004984c98cd5ce2481124e616d654572726f723a20707572706f7365004984c98cd5ce2481104e616d654572726f723a20646174756d004984c98cd5ce2481124e616d654572726f723a20636f6e74657874004984c98cd5ce2481144e616d654572726f723a20526577617264696e67004984c98cd5ce2481154e616d654572726f723a2043657274696679696e67004980080088c010c0200048c0140040108c018c94ccd55cf800899319ab9c49010a496e6465784572726f72004984d5d1000800919000a80091aab9d3754002e1c8d55cf1baa00123253335573e002264c66ae712410a496e6465784572726f72004984d5d0800800919ba548018cd5d028009bb14988cdd2a400866ae814004dd8a4c1

integration-test/test/test_certificate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def test_stake_delegation(self):
2525
builder = TransactionBuilder(self.chain_context)
2626

2727
builder.add_input_address(giver_address)
28-
builder.add_output(TransactionOutput(address, 440000000000))
28+
builder.add_output(TransactionOutput(address, 44000000000))
2929

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

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import os
2+
import time
3+
4+
import cbor2
5+
from retry import retry
6+
7+
from pycardano import *
8+
9+
from .base import TEST_RETRIES, TestBase
10+
11+
12+
class TestDelegation(TestBase):
13+
@retry(tries=TEST_RETRIES, backoff=1.5, delay=6, jitter=(0, 4))
14+
def test_stake_delegation(self):
15+
with open("./plutus_scripts/pass_certifying_and_rewarding.plutus", "r") as f:
16+
script_hex = f.read()
17+
stake_script = PlutusV2Script(bytes.fromhex(script_hex))
18+
cert_script_hash = plutus_script_hash(stake_script)
19+
address = Address(
20+
self.payment_key_pair.verification_key.hash(),
21+
cert_script_hash,
22+
self.NETWORK,
23+
)
24+
25+
utxos = self.chain_context.utxos(address)
26+
27+
if not utxos:
28+
giver_address = Address(self.payment_vkey.hash(), network=self.NETWORK)
29+
30+
builder = TransactionBuilder(self.chain_context)
31+
32+
builder.add_input_address(giver_address)
33+
builder.add_output(TransactionOutput(address, 44000000000))
34+
35+
signed_tx = builder.build_and_sign([self.payment_skey], giver_address)
36+
37+
print("############### Transaction created ###############")
38+
print(signed_tx)
39+
print(signed_tx.to_cbor_hex())
40+
print("############### Submitting transaction ###############")
41+
self.chain_context.submit_tx(signed_tx)
42+
43+
time.sleep(3)
44+
45+
stake_credential = StakeCredential(cert_script_hash)
46+
stake_registration = StakeRegistration(stake_credential)
47+
pool_hash = PoolKeyHash(bytes.fromhex(os.environ.get("POOL_ID").strip()))
48+
stake_delegation = StakeDelegation(stake_credential, pool_keyhash=pool_hash)
49+
50+
builder = TransactionBuilder(self.chain_context)
51+
52+
builder.add_input_address(address)
53+
builder.add_output(TransactionOutput(address, 35000000))
54+
builder.certificates = [stake_registration, stake_delegation]
55+
redeemer = Redeemer(0)
56+
builder.add_certificate_script(stake_script, redeemer=redeemer)
57+
58+
signed_tx = builder.build_and_sign(
59+
[self.payment_key_pair.signing_key],
60+
address,
61+
)
62+
63+
print("############### Transaction created ###############")
64+
print(signed_tx)
65+
print(signed_tx.to_cbor_hex())
66+
print("############### Submitting transaction ###############")
67+
self.chain_context.submit_tx(signed_tx)
68+
69+
70+
# time.sleep(8)
71+
#
72+
# builder = TransactionBuilder(self.chain_context)
73+
#
74+
# builder.add_input_address(address)
75+
#
76+
# stake_address = Address(
77+
# staking_part=cert_script_hash,
78+
# network=self.NETWORK,
79+
# )
80+
#
81+
# builder.withdrawals = Withdrawals({bytes(stake_address): 0})
82+
#
83+
# builder.add_output(TransactionOutput(address, 1000000))
84+
# redeemer = Redeemer(0)
85+
# builder.add_withdrawal_script(stake_script, redeemer=redeemer)
86+
#
87+
# signed_tx = builder.build_and_sign(
88+
# [self.payment_key_pair.signing_key],
89+
# address,
90+
# )
91+
#
92+
# print("############### Transaction created ###############")
93+
# print(signed_tx)
94+
# print(signed_tx.to_cbor_hex())
95+
# print("############### Submitting transaction ###############")
96+
# self.chain_context.submit_tx(signed_tx)
97+
#

pycardano/plutus.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@ class RedeemerTag(CBORSerializable, Enum):
945945

946946
SPEND = 0
947947
MINT = 1
948-
CERT = 2
948+
CERTIFICATE = 2
949949
WITHDRAWAL = 3
950950

951951
def to_primitive(self) -> int:

pycardano/txbuilder.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ class TransactionBuilder:
159159
field(init=False, default_factory=lambda: [])
160160
)
161161

162+
_certificate_script_to_redeemers: List[Tuple[ScriptType, Optional[Redeemer]]] = (
163+
field(init=False, default_factory=lambda: [])
164+
)
165+
162166
_inputs_to_scripts: Dict[UTxO, ScriptType] = field(
163167
init=False, default_factory=lambda: {}
164168
)
@@ -384,6 +388,49 @@ def add_withdrawal_script(
384388
self._withdrawal_script_to_redeemers.append((script, redeemer))
385389
return self
386390

391+
def add_certificate_script(
392+
self,
393+
script: Union[
394+
UTxO, NativeScript, PlutusV1Script, PlutusV2Script, PlutusV3Script
395+
],
396+
redeemer: Optional[Redeemer] = None,
397+
) -> TransactionBuilder:
398+
"""Add a certificate script along with its redeemer to this transaction.
399+
WARNING: The order of operations matters.
400+
The index of the redeemer will be set to the index of the last certificate added.
401+
402+
Args:
403+
script (Union[UTxO, PlutusV1Script, PlutusV2Script, PlutusV3Script]): A plutus script.
404+
redeemer (Optional[Redeemer]): A plutus redeemer to unlock the UTxO.
405+
406+
Returns:
407+
TransactionBuilder: Current transaction builder.
408+
"""
409+
if redeemer:
410+
if redeemer.tag is not None and redeemer.tag != RedeemerTag.CERTIFICATE:
411+
raise InvalidArgumentException(
412+
f"Expect the redeemer tag's type to be {RedeemerTag.CERTIFICATE}, "
413+
f"but got {redeemer.tag} instead."
414+
)
415+
assert self.certificates is not None and len(self.certificates) >= 1, (
416+
"self.certificates is None. redeemer.index needs to be set to the index of the corresponding"
417+
"certificate (defaulting to the last certificate) however no certificates could be found"
418+
)
419+
redeemer.index = len(self.certificates) - 1
420+
redeemer.tag = RedeemerTag.CERTIFICATE
421+
self._consolidate_redeemer(redeemer)
422+
423+
if isinstance(script, UTxO):
424+
assert script.output.script is not None
425+
self._certificate_script_to_redeemers.append(
426+
(script.output.script, redeemer)
427+
)
428+
self.reference_inputs.add(script)
429+
self._reference_scripts.append(script.output.script)
430+
else:
431+
self._certificate_script_to_redeemers.append((script, redeemer))
432+
return self
433+
387434
def add_input_address(self, address: Union[Address, str]) -> TransactionBuilder:
388435
"""Add an address to transaction's input address.
389436
Unlike :meth:`add_input`, which deterministically adds a UTxO to the transaction's inputs, `add_input_address`
@@ -472,6 +519,9 @@ def all_scripts(self) -> List[ScriptType]:
472519
for s, _ in self._withdrawal_script_to_redeemers:
473520
scripts[script_hash(s)] = s
474521

522+
for s, _ in self._certificate_script_to_redeemers:
523+
scripts[script_hash(s)] = s
524+
475525
return list(scripts.values())
476526

477527
@property
@@ -497,6 +547,7 @@ def _redeemer_list(self) -> List[Redeemer]:
497547
[r for r in self._inputs_to_redeemers.values() if r is not None]
498548
+ [r for _, r in self._minting_script_to_redeemers if r is not None]
499549
+ [r for _, r in self._withdrawal_script_to_redeemers if r is not None]
550+
+ [r for _, r in self._certificate_script_to_redeemers if r is not None]
500551
)
501552

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

883936
if self.mint:
884937
sorted_mint_policies = sorted(self.mint.keys(), key=lambda x: x.to_cbor())

test/pycardano/test_txbuilder.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,88 @@ def test_tx_builder_certificates(chain_context):
13631363
assert expected == tx_body.to_primitive()
13641364

13651365

1366+
def test_tx_builder_certificates_script(chain_context):
1367+
tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])])
1368+
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
1369+
sender_address = Address.from_primitive(sender)
1370+
1371+
plutus_script = PlutusV2Script(b"dummy test script")
1372+
script_hash = plutus_script_hash(plutus_script)
1373+
1374+
stake_credential = StakeCredential(script_hash)
1375+
1376+
pool_hash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE)
1377+
1378+
stake_registration = StakeRegistration(stake_credential)
1379+
1380+
stake_delegation = StakeDelegation(stake_credential, pool_hash)
1381+
1382+
# Add sender address as input
1383+
tx_builder.add_input_address(sender).add_output(
1384+
TransactionOutput.from_primitive([sender, 500000])
1385+
)
1386+
1387+
tx_builder.certificates = [stake_registration, stake_delegation]
1388+
redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000))
1389+
tx_builder.add_certificate_script(plutus_script, redeemer=redeemer)
1390+
tx_builder.ttl = 123456
1391+
1392+
tx_builder.build(change_address=sender_address)
1393+
tx_builder.use_redeemer_map = False
1394+
witness = tx_builder.build_witness_set()
1395+
assert [redeemer] == witness.redeemer
1396+
assert witness.redeemer[0].index == 1
1397+
assert [plutus_script] == witness.plutus_v2_script
1398+
1399+
1400+
def test_tx_builder_cert_redeemer_wrong_tag(chain_context):
1401+
tx_builder = TransactionBuilder(chain_context)
1402+
plutus_script = PlutusV2Script(b"dummy test script")
1403+
redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000))
1404+
redeemer.tag = RedeemerTag.MINT
1405+
with pytest.raises(InvalidArgumentException) as e:
1406+
tx_builder.add_certificate_script(plutus_script, redeemer=redeemer)
1407+
1408+
1409+
def test_add_cert_script_from_utxo(chain_context):
1410+
tx_builder = TransactionBuilder(chain_context)
1411+
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"
1412+
sender_address = Address.from_primitive(sender)
1413+
plutus_script = PlutusV2Script(b"dummy test script")
1414+
script_hash = plutus_script_hash(plutus_script)
1415+
script_address = Address(script_hash)
1416+
existing_script_utxo = UTxO(
1417+
TransactionInput.from_primitive(
1418+
[
1419+
"41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7",
1420+
1,
1421+
]
1422+
),
1423+
TransactionOutput(script_address, 1234567, script=plutus_script),
1424+
)
1425+
1426+
stake_credential = StakeCredential(script_hash)
1427+
pool_hash = PoolKeyHash(b"1" * POOL_KEY_HASH_SIZE)
1428+
stake_registration = StakeRegistration(stake_credential)
1429+
stake_delegation = StakeDelegation(stake_credential, pool_hash)
1430+
tx_builder.certificates = [stake_registration, stake_delegation]
1431+
tx_builder.add_input_address(sender).add_output(
1432+
TransactionOutput.from_primitive([sender, 500000])
1433+
)
1434+
1435+
redeemer = Redeemer(PlutusData(), ExecutionUnits(100000, 1000000))
1436+
tx_builder.add_certificate_script(existing_script_utxo, redeemer=redeemer)
1437+
tx_builder.ttl = 123456
1438+
1439+
tx_body = tx_builder.build(change_address=sender_address)
1440+
tx_builder.use_redeemer_map = False
1441+
witness = tx_builder.build_witness_set()
1442+
assert witness.plutus_data is None
1443+
assert [redeemer] == witness.redeemer
1444+
assert witness.plutus_v2_script is None
1445+
assert [existing_script_utxo.input] == tx_body.reference_inputs
1446+
1447+
13661448
def test_tx_builder_stake_pool_registration(chain_context, pool_params):
13671449
tx_builder = TransactionBuilder(chain_context, [RandomImproveMultiAsset([0, 0])])
13681450
sender = "addr_test1vrm9x2zsux7va6w892g38tvchnzahvcd9tykqf3ygnmwtaqyfg52x"

0 commit comments

Comments
 (0)