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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

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

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.97%. Comparing base (2122c39) to head (304375c).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #451      +/-   ##
==========================================
- Coverage   89.98%   89.97%   -0.02%     
==========================================
  Files          33       33              
  Lines        4854     4867      +13     
  Branches      733      736       +3     
==========================================
+ Hits         4368     4379      +11     
- Misses        314      315       +1     
- Partials      172      173       +1     

☔ 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.

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.

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