diff --git a/pycardano/txbuilder.py b/pycardano/txbuilder.py index e296a510..276bb8be 100644 --- a/pycardano/txbuilder.py +++ b/pycardano/txbuilder.py @@ -206,6 +206,10 @@ class TransactionBuilder: init=False, default_factory=lambda: [] ) + _reference_script_inputs: Set[TransactionInput] = field( + init=False, default_factory=lambda: set() + ) + _should_estimate_execution_units: Optional[bool] = field(init=False, default=None) def add_input(self, utxo: UTxO) -> TransactionBuilder: @@ -219,9 +223,23 @@ def add_input(self, utxo: UTxO) -> TransactionBuilder: """ self.inputs.append(utxo) if utxo.output.script: - self._reference_scripts.append(utxo.output.script) + self._add_reference_script(utxo) return self + def _add_reference_script(self, utxo: UTxO): + """Count ``utxo``'s script toward the reference script fee. + + The ledger charges the scripts held by a transaction's inputs and reference + inputs once per UTxO, however many redeemers resolve their script from it, so + a UTxO that is already counted is skipped. The same script held by two + different UTxOs still counts twice. + """ + assert utxo.output.script is not None + if utxo.input in self._reference_script_inputs: + return + self._reference_script_inputs.add(utxo.input) + self._reference_scripts.append(utxo.output.script) + def _consolidate_redeemer(self, redeemer): if self._should_estimate_execution_units is None: if redeemer.ex_units: @@ -342,7 +360,7 @@ def add_script_input( if candidate_utxo is not None and candidate_utxo != utxo: self.reference_inputs.add(candidate_utxo) - self._reference_scripts.append(candidate_script) + self._add_reference_script(candidate_utxo) break if not found_valid_script: raise InvalidArgumentException( @@ -380,7 +398,7 @@ def add_minting_script( assert script.output.script is not None self._minting_script_to_redeemers.append((script.output.script, redeemer)) self.reference_inputs.add(script) - self._reference_scripts.append(script.output.script) + self._add_reference_script(script) else: self._minting_script_to_redeemers.append((script, redeemer)) return self @@ -414,7 +432,7 @@ def add_withdrawal_script( (script.output.script, redeemer) ) self.reference_inputs.add(script) - self._reference_scripts.append(script.output.script) + self._add_reference_script(script) else: self._withdrawal_script_to_redeemers.append((script, redeemer)) return self @@ -455,7 +473,7 @@ def add_certificate_script( (script.output.script, redeemer) ) self.reference_inputs.add(script) - self._reference_scripts.append(script.output.script) + self._add_reference_script(script) else: self._certificate_script_to_redeemers.append((script, redeemer)) return self diff --git a/test/pycardano/test_txbuilder.py b/test/pycardano/test_txbuilder.py index 9b924405..d53ae5cb 100644 --- a/test/pycardano/test_txbuilder.py +++ b/test/pycardano/test_txbuilder.py @@ -2932,6 +2932,80 @@ def test_add_withdrawal_script_from_utxo(chain_context): assert existing_script_utxo.input in tx_body.reference_inputs +def test_reference_script_shared_by_spend_and_withdrawal_counted_once(chain_context): + """A reference script UTxO used by several redeemers is a single reference input, + so the ledger charges its size once toward the reference script fee.""" + tx_builder = TransactionBuilder(chain_context) + plutus_script = PlutusV2Script(b"dummy shared script") + script_hash = plutus_script_hash(plutus_script) + datum = PlutusData() + script_utxo = UTxO( + TransactionInput.from_primitive( + ["18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", 0] + ), + TransactionOutput(Address(script_hash), 10000000, datum_hash=datum.hash()), + ) + reference_utxo = UTxO( + TransactionInput.from_primitive( + ["41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", 1] + ), + TransactionOutput(Address(script_hash), 1234567, script=plutus_script), + ) + tx_builder.add_script_input( + script_utxo, + script=reference_utxo, + datum=datum, + redeemer=Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)), + ) + stake_address = Address( + payment_part=None, staking_part=script_hash, network=chain_context.network + ) + tx_builder.withdrawals = Withdrawals({bytes(stake_address): 0}) + tx_builder.add_withdrawal_script( + reference_utxo, Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)) + ) + + assert tx_builder.reference_inputs == {reference_utxo} + assert tx_builder._ref_script_size() == len(plutus_script) + + +def test_same_script_in_two_reference_utxos_counted_per_utxo(chain_context): + """The ledger charges every reference input, so one script held by two reference + UTxOs counts twice.""" + tx_builder = TransactionBuilder(chain_context) + plutus_script = PlutusV2Script(b"dummy shared script") + script_hash = plutus_script_hash(plutus_script) + datum = PlutusData() + for index in (0, 1): + tx_builder.add_script_input( + UTxO( + TransactionInput.from_primitive( + [ + "18cbe6cadecd3f89b60e08e68e5e6c7d72d730aaa1ad21431590f7e6643438ef", + index, + ] + ), + TransactionOutput( + Address(script_hash), 10000000, datum_hash=datum.hash() + ), + ), + script=UTxO( + TransactionInput.from_primitive( + [ + "41cb004bec7051621b19b46aea28f0657a586a05ce2013152ea9b9f1a5614cc7", + index, + ] + ), + TransactionOutput(Address(script_hash), 1234567, script=plutus_script), + ), + datum=datum, + redeemer=Redeemer(PlutusData(), ExecutionUnits(1000000, 1000000)), + ) + + assert len(tx_builder.reference_inputs) == 2 + assert tx_builder._ref_script_size() == 2 * len(plutus_script) + + def test_withdrawal_script_wrong_redeemer_tag(chain_context): """Test that withdrawal script with wrong redeemer tag raises exception.""" plutus_script = PlutusV2Script(b"dummy withdrawal script")