Skip to content

Commit 86abb73

Browse files
authored
Merge pull request #2913 from IntersectMBO/minimize_locked_funds_plutus
feat(plutus): minimize locked funds on spend script addresses
2 parents 05d0c49 + 2541431 commit 86abb73

21 files changed

+560
-220
lines changed

cardano_node_tests/tests/plutus_common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,14 @@ class ExecutionCost:
9090
# Scripts execution cost for Txs with single UTxO input and single Plutus script
9191
ALWAYS_FAILS_COST = ExecutionCost(per_time=476_468, per_space=1_700, fixed_cost=133)
9292
ALWAYS_SUCCEEDS_COST = ExecutionCost(per_time=368_100, per_space=1_700, fixed_cost=125)
93-
GUESSING_GAME_COST = ExecutionCost(per_time=236_715_138, per_space=870_842, fixed_cost=67_315)
93+
GUESSING_GAME_COST = ExecutionCost(per_time=282_016_214, per_space=1_034_516, fixed_cost=80_025)
9494
GUESSING_GAME_UNTYPED_COST = ExecutionCost(per_time=4_985_806, per_space=11_368, fixed_cost=1_016)
9595
# TODO: fix once context equivalence tests can run again
9696
CONTEXT_EQUIVALENCE_COST = ExecutionCost(per_time=100_000_000, per_space=1_000_00, fixed_cost=947)
9797

9898
ALWAYS_FAILS_V2_COST = ExecutionCost(per_time=230_100, per_space=1_100, fixed_cost=81)
9999
ALWAYS_SUCCEEDS_V2_COST = ExecutionCost(per_time=230_100, per_space=1_100, fixed_cost=81)
100-
GUESSING_GAME_V2_COST = ExecutionCost(per_time=168_868_800, per_space=540_612, fixed_cost=43_369)
100+
GUESSING_GAME_V2_COST = ExecutionCost(per_time=200_253_161, per_space=637_676, fixed_cost=51_233)
101101
GUESSING_GAME_UNTYPED_V2_COST = ExecutionCost(
102102
per_time=4_985_806, per_space=11_368, fixed_cost=1_016
103103
)

cardano_node_tests/tests/tests_plutus/spend_build.py

Lines changed: 52 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ def _build_fund_script(
2020
payment_addr: clusterlib.AddressRecord,
2121
dst_addr: clusterlib.AddressRecord,
2222
plutus_op: plutus_common.PlutusOp,
23+
amount: int,
2324
tokens: list[clusterlib_utils.Token] | None = None, # tokens must already be in `payment_addr`
2425
tokens_collateral: list[clusterlib_utils.Token]
2526
| None = None, # tokens must already be in `payment_addr`
@@ -31,8 +32,6 @@ def _build_fund_script(
3132
"""
3233
assert plutus_op.execution_cost # for mypy
3334

34-
script_fund = 200_000_000
35-
3635
stokens = tokens or ()
3736
ctokens = tokens_collateral or ()
3837

@@ -53,7 +52,7 @@ def _build_fund_script(
5352

5453
script_txout = plutus_common.txout_factory(
5554
address=script_address,
56-
amount=script_fund,
55+
amount=amount,
5756
plutus_op=plutus_op,
5857
embed_datum=embed_datum,
5958
)
@@ -97,7 +96,7 @@ def _build_fund_script(
9796
script_utxos = clusterlib.filter_utxos(utxos=out_utxos, utxo_ix=utxo_ix_offset)
9897
assert script_utxos, "No script UTxO"
9998

100-
assert clusterlib.calculate_utxos_balance(utxos=script_utxos) == script_fund, (
99+
assert clusterlib.calculate_utxos_balance(utxos=script_utxos) == amount, (
101100
f"Incorrect balance for script address `{script_address}`"
102101
)
103102

@@ -143,13 +142,44 @@ def _build_spend_locked_txin( # noqa: C901
143142
expect_failure: bool = False,
144143
script_valid: bool = True,
145144
submit_tx: bool = True,
145+
witness_override: int | None = None,
146146
) -> tuple[str, clusterlib.TxRawOutput | None, list]:
147147
"""Spend the locked UTxO.
148148
149149
Uses `cardano-cli transaction build` command for building the transactions.
150150
"""
151151
tx_files = tx_files or clusterlib.TxFiles()
152152
spent_tokens = tokens or ()
153+
spent_tokens_dict = {r.coin: r for r in spent_tokens}
154+
available_tokens = [
155+
clusterlib_utils.Token(coin=r.coin, amount=r.amount)
156+
for r in script_utxos
157+
if r.coin != clusterlib.DEFAULT_COIN
158+
]
159+
160+
script_amount = clusterlib.calculate_utxos_balance(utxos=script_utxos)
161+
162+
# Spend all locked funds
163+
if amount == -1:
164+
amount = script_amount
165+
166+
if (script_amount - amount) < 100_000_000:
167+
# Add additional funds to cover fee and Lovelace change for token txouts
168+
fee_txin = next(
169+
r
170+
for r in clusterlib_utils.get_just_lovelace_utxos(
171+
address_utxos=cluster_obj.g_query.get_utxo(address=payment_addr.address)
172+
)
173+
if r.amount >= 100_000_000
174+
)
175+
txins = [
176+
*txins,
177+
fee_txin,
178+
]
179+
tx_files = dataclasses.replace(
180+
tx_files,
181+
signing_key_files=list({*tx_files.signing_key_files, payment_addr.skey_file}),
182+
)
153183

154184
# Change that was calculated manually will be returned to address of the first script.
155185
# The remaining change that is automatically handled by the `build` command will be returned
@@ -181,20 +211,23 @@ def _build_spend_locked_txin( # noqa: C901
181211
]
182212

183213
lovelace_change_needed = False
184-
for token in spent_tokens:
185-
txouts.append(
186-
clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin)
187-
)
188-
# Append change
214+
for token in available_tokens:
215+
spent_amount = 0
189216
script_token_balance = clusterlib.calculate_utxos_balance(
190217
utxos=script_utxos, coin=token.coin
191218
)
192-
if script_token_balance > token.amount:
219+
if stoken := spent_tokens_dict.get(token.coin):
220+
spent_amount = stoken.amount
221+
txouts.append(
222+
clusterlib.TxOut(address=dst_addr.address, amount=spent_amount, coin=stoken.coin)
223+
)
224+
# Append change
225+
if script_token_balance > spent_amount:
193226
lovelace_change_needed = True
194227
txouts.append(
195228
clusterlib.TxOut(
196229
address=script_change_rec.address,
197-
amount=script_token_balance - token.amount,
230+
amount=script_token_balance - spent_amount,
198231
coin=token.coin,
199232
datum_hash=script_change_rec.datum_hash,
200233
)
@@ -220,6 +253,11 @@ def _build_spend_locked_txin( # noqa: C901
220253
txouts=txouts,
221254
script_txins=plutus_txins,
222255
change_address=payment_addr.address,
256+
invalid_hereafter=invalid_hereafter,
257+
invalid_before=invalid_before,
258+
deposit=deposit_amount,
259+
script_valid=script_valid,
260+
witness_override=witness_override,
223261
)
224262
return str(excinfo.value), None, []
225263

@@ -235,6 +273,7 @@ def _build_spend_locked_txin( # noqa: C901
235273
invalid_before=invalid_before,
236274
deposit=deposit_amount,
237275
script_valid=script_valid,
276+
witness_override=witness_override,
238277
)
239278
tx_signed = cluster_obj.g_transaction.sign_tx(
240279
tx_body_file=tx_output.out_file,
@@ -287,13 +326,15 @@ def _build_spend_locked_txin( # noqa: C901
287326
src_address=payment_addr.address,
288327
tx_name=f"{temp_template}_step2",
289328
tx_files=tx_files,
329+
txins=txins,
290330
txouts=txouts,
291331
script_txins=plutus_txins,
292332
change_address=payment_addr.address,
293333
invalid_hereafter=invalid_hereafter,
294334
invalid_before=invalid_before,
295335
deposit=deposit_amount,
296336
script_valid=script_valid,
337+
witness_override=witness_override,
297338
)
298339

299340
cluster_obj.g_transaction.submit_tx(

cardano_node_tests/tests/tests_plutus/spend_raw.py

Lines changed: 84 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ def _fund_script(
2525
dst_addr: clusterlib.AddressRecord,
2626
plutus_op: plutus_common.PlutusOp,
2727
amount: int,
28-
fee_txsize: int = FEE_REDEEM_TXSIZE,
29-
deposit_amount: int = 0,
3028
tokens: list[clusterlib_utils.Token] | None = None, # tokens must already be in `payment_addr`
3129
tokens_collateral: list[clusterlib_utils.Token]
3230
| None = None, # tokens must already be in `payment_addr`
@@ -57,7 +55,7 @@ def _fund_script(
5755

5856
script_txout = plutus_common.txout_factory(
5957
address=script_address,
60-
amount=amount + redeem_cost.fee + fee_txsize + deposit_amount,
58+
amount=amount,
6159
plutus_op=plutus_op,
6260
embed_datum=embed_datum,
6361
)
@@ -126,12 +124,14 @@ def _fund_script(
126124
def _spend_locked_txin( # noqa: C901
127125
temp_template: str,
128126
cluster_obj: clusterlib.ClusterLib,
127+
payment_addr: clusterlib.AddressRecord,
129128
dst_addr: clusterlib.AddressRecord,
130129
script_utxos: list[clusterlib.UTXOData],
131130
collateral_utxos: list[clusterlib.UTXOData],
132131
plutus_op: plutus_common.PlutusOp,
133132
amount: int,
134-
fee_txsize: int = FEE_REDEEM_TXSIZE,
133+
fee_txsize: int | None = None,
134+
deposit_amount: int = 0,
135135
txins: clusterlib.OptionalUTXOData = (),
136136
tx_files: clusterlib.TxFiles | None = None,
137137
invalid_hereafter: int | None = None,
@@ -144,22 +144,35 @@ def _spend_locked_txin( # noqa: C901
144144
"""Spend the locked UTxO."""
145145
assert plutus_op.execution_cost
146146

147+
if fee_txsize is None:
148+
fee_txsize = FEE_REDEEM_TXSIZE
149+
147150
tx_files = tx_files or clusterlib.TxFiles()
148151
spent_tokens = tokens or ()
152+
spent_tokens_dict = {r.coin: r for r in spent_tokens}
153+
available_tokens = [
154+
clusterlib_utils.Token(coin=r.coin, amount=r.amount)
155+
for r in script_utxos
156+
if r.coin != clusterlib.DEFAULT_COIN
157+
]
149158

150-
# Change will be returned to address of the first script
151-
change_rec = script_utxos[0]
159+
script_amount = clusterlib.calculate_utxos_balance(utxos=script_utxos)
160+
161+
# Spend all locked funds
162+
if amount == -1:
163+
amount = script_amount
164+
165+
# Change that was calculated manually will be returned to address of the first script.
166+
# The remaining change that is automatically handled by the `build` command will be returned
167+
# to `payment_addr`, because it would be inaccessible on script address without proper
168+
# datum hash (datum hash is not provided for change that is handled by `build` command).
169+
script_change_rec = script_utxos[0]
152170

153171
redeem_cost = plutus_common.compute_cost(
154172
execution_cost=plutus_op.execution_cost,
155173
protocol_params=cluster_obj.g_query.get_protocol_params(),
156174
)
157175

158-
script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN]
159-
script_lovelace_balance = clusterlib.calculate_utxos_balance(
160-
utxos=[*script_utxos_lovelace, *txins]
161-
)
162-
163176
# Spend the "locked" UTxO
164177

165178
plutus_txins = [
@@ -184,34 +197,75 @@ def _spend_locked_txin( # noqa: C901
184197
txouts = [
185198
clusterlib.TxOut(address=dst_addr.address, amount=amount),
186199
]
187-
# Append change
188-
if script_lovelace_balance > amount + redeem_cost.fee + fee_txsize:
189-
txouts.append(
190-
clusterlib.TxOut(
191-
address=change_rec.address,
192-
amount=script_lovelace_balance - amount - redeem_cost.fee - fee_txsize,
193-
datum_hash=change_rec.datum_hash,
194-
)
195-
)
196200

197-
for token in spent_tokens:
198-
txouts.append(
199-
clusterlib.TxOut(address=dst_addr.address, amount=token.amount, coin=token.coin)
200-
)
201-
# Append change
201+
lovelace_change_needed = False
202+
for token in available_tokens:
203+
spent_amount = 0
202204
script_token_balance = clusterlib.calculate_utxos_balance(
203205
utxos=script_utxos, coin=token.coin
204206
)
205-
if script_token_balance > token.amount:
207+
if stoken := spent_tokens_dict.get(token.coin):
208+
spent_amount = stoken.amount
209+
txouts.append(
210+
clusterlib.TxOut(address=dst_addr.address, amount=spent_amount, coin=stoken.coin)
211+
)
212+
# Append change
213+
if script_token_balance > spent_amount:
214+
lovelace_change_needed = True
206215
txouts.append(
207216
clusterlib.TxOut(
208-
address=change_rec.address,
209-
amount=script_token_balance - token.amount,
217+
address=script_change_rec.address,
218+
amount=script_token_balance - spent_amount,
210219
coin=token.coin,
211-
datum_hash=change_rec.datum_hash,
220+
datum_hash=script_change_rec.datum_hash,
212221
)
213222
)
214223

224+
# Add minimum (+ some) required Lovelace to change Tx output
225+
lovelace_token_change = 0
226+
if lovelace_change_needed:
227+
lovelace_token_change = 4_000_000
228+
txouts.append(
229+
clusterlib.TxOut(
230+
address=script_change_rec.address,
231+
amount=lovelace_token_change,
232+
coin=clusterlib.DEFAULT_COIN,
233+
datum_hash=script_change_rec.datum_hash,
234+
)
235+
)
236+
237+
input_lovelace_balance = clusterlib.calculate_utxos_balance(utxos=[*txins]) + script_amount
238+
funds_needed = amount + redeem_cost.fee + fee_txsize + deposit_amount + lovelace_token_change
239+
240+
if input_lovelace_balance < funds_needed:
241+
# Add additional funds to cover fee and Lovelace change for token txouts
242+
fee_txin = next(
243+
r
244+
for r in clusterlib_utils.get_just_lovelace_utxos(
245+
address_utxos=cluster_obj.g_query.get_utxo(address=payment_addr.address)
246+
)
247+
if r.amount >= 100_000_000
248+
)
249+
txins = [
250+
*txins,
251+
fee_txin,
252+
]
253+
tx_files = dataclasses.replace(
254+
tx_files,
255+
signing_key_files=list({*tx_files.signing_key_files, payment_addr.skey_file}),
256+
)
257+
input_lovelace_balance = clusterlib.calculate_utxos_balance(utxos=[*txins]) + script_amount
258+
259+
# Append change
260+
change_lovelace = input_lovelace_balance - funds_needed
261+
if change_lovelace > 0:
262+
txouts.append(
263+
clusterlib.TxOut(
264+
address=payment_addr.address,
265+
amount=change_lovelace,
266+
)
267+
)
268+
215269
tx_raw_output = cluster_obj.g_transaction.build_raw_tx_bare(
216270
out_file=f"{temp_template}_step2_tx.body",
217271
txins=txins,
@@ -233,6 +287,7 @@ def _spend_locked_txin( # noqa: C901
233287
return "", tx_raw_output
234288

235289
dst_init_balance = cluster_obj.g_query.get_address_balance(dst_addr.address)
290+
script_utxos_lovelace = [u for u in script_utxos if u.coin == clusterlib.DEFAULT_COIN]
236291

237292
if not script_valid:
238293
cluster_obj.g_transaction.submit_tx_bare(tx_file=tx_signed)

0 commit comments

Comments
 (0)