Skip to content

Add support for NonEmptyOrderedSet in Plutus_data #451

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 9, 2025

Conversation

KINGH242
Copy link
Contributor

@KINGH242 KINGH242 commented Aug 2, 2025

This pull request improves handling of indefinite lists, update encoding of Plutus data, and expanded test coverage. Key changes include updates to the OrderedSet class to support indefinite lists, modifications to the transaction builder and utilities for more robust Plutus data handling, and update tests to validate these updates.

Enhancements to OrderedSet and Serialization:

  • Updated the OrderedSet class to inherit from IndefiniteList and added support for indefinite list detection and representation. This ensures compatibility with CBOR encoding for both definite and indefinite lists (pycardano/serialization.py). [1] [2] [3]
  • Extended NonEmptyOrderedSet to accept IndefiniteList as input, improving its flexibility (pycardano/serialization.py).

Improvements in Plutus Data Handling:

  • Modified TransactionWitnessSet to support NonEmptyOrderedSet for plutus_data (pycardano/witness.py). [1] [2]
  • Updated the build_witness_set method to use NonEmptyOrderedSet for Plutus data (pycardano/txbuilder.py). [1] [2] [3]

Enhancements to Utilities:

  • Refactored the script_data_hash function to handle NonEmptyOrderedSet for datums and ensure canonical CBOR encoding for cost models (pycardano/utils.py).

Test Coverage Expansion:

  • Added new test cases to validate script_data_hash behavior with RedeemerMap and updated expected hash values to reflect changes in encoding logic (test/pycardano/test_util.py).
  • Adjusted existing tests to align with the updated handling of Plutus data and indefinite lists (test/pycardano/test_txbuilder.py).

Copy link

codecov bot commented Aug 2, 2025

Codecov Report

❌ Patch coverage is 93.18182% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.85%. Comparing base (2122c39) to head (65eedc8).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
pycardano/utils.py 50.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #451      +/-   ##
==========================================
- Coverage   89.98%   89.85%   -0.13%     
==========================================
  Files          33       33              
  Lines        4854     4881      +27     
  Branches      733      739       +6     
==========================================
+ Hits         4368     4386      +18     
- Misses        314      319       +5     
- Partials      172      176       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@KINGH242
Copy link
Contributor Author

KINGH242 commented Aug 2, 2025

This fixes #450

Copy link
Collaborator

@cffls cffls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR! I have a basic understanding of what you are trying to accomplish. My main concern was that, with this change, all plutus data will be converted to NonEmptyOrderedSet, regardless of their original format. This might break all transactions generated pre-conway. Since you've fixed the serialization part, I don't think the forced conversion is required in script_data_hash, but happy to discuss further or help with debugging if you have a concrete example of failure test cases.

if not redeemers:
redeemer_bytes = cbor2.dumps({}, default=default_encoder)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should just check if the redeemer is actually None, and if so, assign an empty redeemer instance to it: redeemers = Redeemers(), similar to the implementation here, and the encoding will be automatically handled.

Suggested change
redeemer_bytes = cbor2.dumps({}, default=default_encoder)
if redeemers is None:
redeemers = Redeemers()

Empty Redeemers() will be encoded as A0.

Comment on lines 263 to 266
if isinstance(datums, list):
# If datums is a NonEmptyOrderedSet, convert it to a shallow primitive representation
# to ensure correct CBOR encoding
datums = NonEmptyOrderedSet(datums)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it is a good idea to convert it to NonEmptyOrderedSet. Ideally, script_data_hash should never change its input, because all items, including redeemers and datums, should match exactly to their representation in the witness set. See this: https://github.com/IntersectMBO/cardano-ledger/blob/1f0c6a4eaa4fb8f937c30a4608a4fafedaca216e/eras/conway/impl/cddl-files/conway.cddl#L449-L451

@@ -156,22 +166,39 @@ def test_script_data_hash():
redeemers = [Redeemer(unit, ExecutionUnits(1000000, 1000000))]
redeemers[0].tag = RedeemerTag.SPEND
assert ScriptDataHash.from_primitive(
"032d812ee0731af78fe4ec67e4d30d16313c09e6fb675af28f825797e8b5621d"
"2ad155a692b0ddb6752df485de0a6bdb947757f9f998ff34a6f4b06ca0664fbe"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to violate the purpose of unit test. The test was written to ensure a known working datum hash remains working. If it has to change, it means the code changes have broken something. If the purpose is to test the case where datums is wrapped in NonEmptyOrderedSet, please create a new test.

def test_script_data_hash_datum_only():
unit = Unit()
assert ScriptDataHash.from_primitive(
"2f50ea2546f8ce020ca45bfcf2abeb02ff18af2283466f888ae489184b3d2d39"
"264ea21d9904cd72ce5038fa60e0ddd0859383f7fbf60ecec6df22e4c4e34a1f"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, changing hash in unit test isn't a good idea.

@KINGH242
Copy link
Contributor Author

KINGH242 commented Aug 2, 2025

Thanks for the quick feedback. I will work on changes and revert.

Comment on lines 263 to 269
if not isinstance(datums, NonEmptyOrderedSet):
# If datums is not a NonEmptyOrderedSet, handle it as a list
datum_bytes = cbor2.dumps(datums, default=default_encoder)
else:
datum_bytes = cbor2.dumps(
datums.to_shallow_primitive(), default=default_encoder
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see why this change is needed. The default_encoder will be able to call to_shallow_primitive when the datum is not a list. We can keep the original implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this was needed the way it is because NonEmptyOrderedSet also inherits from other types that might cause incorrect behavior here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for checking. I think there might be some issue in the way the cbor encodes the OrderedSet. I will play with it a bit and see if there is a better solution.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed this issue by removing list type from OrderedSet. Having list type will enable cbor to bypass to_shallow_primitve() and directly encode the object as a list.

cost_models_bytes = cbor2.dumps(
cost_models,
default=default_encoder,
canonical=True, # Ensures definite length encoding and canonical map keys
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might break existing unit tests.

def test_script_data_hash_datum_only():
unit = Unit()
assert ScriptDataHash.from_primitive(
"2f50ea2546f8ce020ca45bfcf2abeb02ff18af2283466f888ae489184b3d2d39"
"244926529564c04ffdea89005076a6b6aac5e4a2f38182cd48bfbc734b3be296"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, I don't think we should allow the hash to change in the unit test.

) == script_data_hash(redeemers=[], datums=[unit])


def test_script_data_hash_redeemer_only():
unit = Unit()
redeemers = []
assert ScriptDataHash.from_primitive(
"a88fe2947b8d45d1f8b798e52174202579ecf847b8f17038c7398103df2d27b0"
"9eb0251b2e85b082c3706a3e79b4cf2a2e96f936e912a398591e2486c757f8c1"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above.

@KINGH242
Copy link
Contributor Author

KINGH242 commented Aug 3, 2025

I was able to fix the other issues, thought I reverted the hashes in the previous one but I maybe didn't check them properly. Now script_data_hash can handle any of the various scenarios.

@cffls
Copy link
Collaborator

cffls commented Aug 8, 2025

Pushed a more generic fix. @KINGH242 Could you please check if this new patch works for all your use cases?

@KINGH242
Copy link
Contributor Author

KINGH242 commented Aug 9, 2025

Yes, this still works now. I just added one more thing as I discovered a new edge case with another transaction where plutus_data is encoded as an IndefiniteList.

Copy link
Collaborator

@cffls cffls left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!

@cffls cffls merged commit 17a0d34 into Python-Cardano:main Aug 9, 2025
12 of 13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants