Skip to content
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

Refactor witnesses indexing functions to have the indexing logic in one place #697

Merged

Conversation

carbolymer
Copy link
Contributor

Changelog

- description: |
    Refactor witnesses indexing functions to have the indexing logic in one place
# uncomment types applicable to the change:
  type:
  # - feature        # introduces a new feature
  # - breaking       # the API has changed in a breaking way
  # - compatible     # the API has changed but is non-breaking
  # - optimisation   # measurable performance improvements
   - refactoring    # QoL changes
  # - bugfix         # fixes a defect
  # - test           # fixes/modifies tests
  # - maintenance    # not directly related to the code
  # - release        # related to a new release preparation
  # - documentation  # change in code docs, haddocks...

Context

It was annoying to look up how witnesses are indexed: the logic was copied into multiple places, away from the datatypes which store them.

This PR moves them into one place, simplifies witness updating code in Cardano.Api.Fees and fixes one place which had wrong witness ordering.

Checklist

  • Commit sequence broadly makes sense and commits have useful messages
  • New tests are added if needed and existing tests are updated. See Running tests for more details
  • Self-reviewed the diff

Comment on lines -3373 to -3545
| let proposalsList = toList proposalProcedures
, (ix, proposal) <- zip [0 ..] proposalsList
Copy link
Contributor Author

@carbolymer carbolymer Nov 28, 2024

Choose a reason for hiding this comment

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

This one was incorrect. proposals from mScriptWitnesses not present in proposalProcedures were not added to the list. This issue was fixed in: #602, but it looks like this place was ommited by accident.

@carbolymer carbolymer force-pushed the mgalazyn/refactor/move-wit-indexing-logic-into-one-place branch from 4d209e2 to c50555d Compare November 28, 2024 14:33
-> [(ScriptWitnessIndex, TxIn, Witness WitCtxTxIn era)]
txInsToIndexed txins =
[ (ScriptWitnessIndexTxIn ix, txIn, witness)
| (ix, (txIn, BuildTxWith witness)) <- zip [0 ..] $ orderTxIns txins
Copy link
Contributor

Choose a reason for hiding this comment

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

This will now pattern match on key witnesses because we don't pattern match on ScriptWitness:

   scriptWitnessesTxIns txins =
      [ (ScriptWitnessIndexTxIn ix, AnyScriptWitness witness)
      | -- The tx ins are indexed in the map order by txid
      (ix, (_, BuildTxWith (ScriptWitness _ witness))) <-
        zip [0 ..] (orderTxIns txins)

We need to be extremely careful with the refactors here.

Copy link
Contributor Author

@carbolymer carbolymer Nov 28, 2024

Choose a reason for hiding this comment

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

Oh good catch. However, I've copied the indexing logic from Fees.hs:1464:

            [ (txin, BuildTxWith <$> wit')
            | -- The tx ins are indexed in the map order by txid
            (ix, (txin, BuildTxWith wit)) <- zip [0 ..] (orderTxIns txins)
            , let wit' = case wit of
                    KeyWitness{} -> Right wit
                    ScriptWitness ctx witness -> ScriptWitness ctx <$> witness'
                     where
                      witness' = substituteExecUnits (ScriptWitnessIndexTxIn ix) witness
            ]

and there indices were generated for both ScriptWitness and KeyWitness!

Which is correct then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

substituteExecutionUnits would strip TxIns with KeyWitneses from TxBodyContent if we index only scripts. That sounds wrong.
But giving script indices to key witnesses does not seem right as well.

Copy link
Contributor Author

@carbolymer carbolymer Nov 28, 2024

Choose a reason for hiding this comment

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

I think we should keep both: calculate indices for scripts only and update only script inputs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok so the actual issue is we do need all the indexes but they are not all necessarily ScriptWitnessIndexes. Some may be witnessed by a key and technically shouldn't have a corresponding ScriptWitnessIndex. However I'm not seeing a simple way to disambiguate this without making the code unnecessarily complicated.

Copy link
Contributor

@Jimbo4350 Jimbo4350 left a comment

Choose a reason for hiding this comment

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

LGTM I would consider renaming the indexing functions however. We need to do a follow up PR to prevent the abuse of ScriptWitnessIndex.

cardano-api/internal/Cardano/Api/Fees.hs Outdated Show resolved Hide resolved
-> [(ScriptWitnessIndex, TxIn, Witness WitCtxTxIn era)]
txInsToIndexed txins =
[ (ScriptWitnessIndexTxIn ix, txIn, witness)
| (ix, (txIn, BuildTxWith witness)) <- zip [0 ..] $ orderTxIns txins
Copy link
Contributor

Choose a reason for hiding this comment

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

Ok so the actual issue is we do need all the indexes but they are not all necessarily ScriptWitnessIndexes. Some may be witnessed by a key and technically shouldn't have a corresponding ScriptWitnessIndex. However I'm not seeing a simple way to disambiguate this without making the code unnecessarily complicated.

cardano-api/internal/Cardano/Api/Tx/Body.hs Outdated Show resolved Hide resolved
@@ -1232,6 +1268,20 @@ deriving instance Eq (TxCertificates build era)

deriving instance Show (TxCertificates build era)

-- | Index certificates with witnesses by the order they appear in the list (in the transaction). If there
-- are multiple witnesses for the credential, the last one is returned.
Copy link
Contributor

Choose a reason for hiding this comment

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

If there are multiple witnesses for the credential, the last one is returned

Can you elaborate on this? What does this mean/why is this ok?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, I am not sure. I've preserved the original logic without thinking much about it:

    mapScriptWitnessesCertificates
      ( TxCertificates
          supported
          certs
          (BuildTxWith witnesses)
        ) =
        let mappedScriptWitnesses
              :: [(StakeCredential, Either (TxBodyErrorAutoBalance era) (Witness WitCtxStake era))]
            mappedScriptWitnesses =
              [ (stakecred, ScriptWitness ctx <$> witness')
              | -- The certs are indexed in list order
              (ix, cert) <- zip [0 ..] certs
              , stakecred <- maybeToList (selectStakeCredentialWitness cert)
              , ScriptWitness ctx witness <-
                  maybeToList (List.lookup stakecred witnesses)
              , let witness' = substituteExecUnits (ScriptWitnessIndexCertificate ix) witness
              ]
         in TxCertificates supported certs . BuildTxWith
              <$> traverse
                ( \(sCred, eScriptWitness) ->
                    case eScriptWitness of
                      Left e -> Left e
                      Right wit -> Right (sCred, wit)
                )
                mappedScriptWitnesses

so this bit:

ScriptWitness ctx witness <- maybeToList (List.lookup stakecred witnesses)

returns only the first witness. The comment mentioning the last witness is wrong.

However:

  TxCertificates
    :: ShelleyBasedEra era
    -> [Certificate era]
    -> BuildTxWith build [(StakeCredential, Witness WitCtxStake era)]
    -- ^ There can be more than one script witness per stake credential
    -> TxCertificates build era

So the fact that we only use the first witness for the stake credential, feels wrong here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually it should only matter in fee calculation in estimateTransactionKeyWitnessCount, because we were counting witnesses there. So if we used only first witness for the stake credential, the fee was underestimated.

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've included all witnesses for the stake now.

List.lookup stakecred witnesses
scriptWitnessesCertificates txc =
[ (ix, AnyScriptWitness witness)
| (ix, _, _, ScriptWitness _ witness) <- txCertificatesToIndexed txc
Copy link
Contributor

Choose a reason for hiding this comment

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

It's clearer here that we are using the indexing functions in two contexts:

  1. Script witness execution unit substitution
  2. Retrieval of script witnesses

I would argue having these as separate functions would make this code clearer. However that is not a task for this PR.

In the case of 1. we keep the key witnesses because we are updating the tx body with the set of all things being witnessed. Because of this we abuse ScriptWitnessIndex and pair it with things that are actually key witnessed.

-> [(ScriptWitnessIndex, L.ProposalProcedure (ShelleyLedgerEra era), ScriptWitness WitCtxStake era)]
txProposalProceduresToIndexed TxProposalProceduresNone = []
txProposalProceduresToIndexed txpp@(TxProposalProcedures _ (BuildTxWith witnesses)) = do
let allProposalsList = toList $ convProposalProcedures txpp
Copy link
Contributor

Choose a reason for hiding this comment

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

IIRC you were suggesting a pattern synonym to prevent users from having to call convProposalProcedures?

Copy link
Contributor Author

@carbolymer carbolymer Dec 30, 2024

Choose a reason for hiding this comment

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

Yes. I was suggesting:

  1. Not exporting TxProposalProcedures constructors and prefix them with Unsafe
  2. Add the following unidirectional pattern:
pattern TxProposalProcedures :: OSet (L.ProposalProcedure (ShelleyLedgerEra era)) -> TxProposalProcedures build era
pattern TxProposalProcedures oset <- (convProposalProcedures -> oset)

(export of convProposalProcedures will be unnecessary)
3. Make usage of mkTxProposalProcedures the only way to construct the value.

That would make API safer to use.

Copy link
Contributor

Choose a reason for hiding this comment

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

LGTM, feel free to add it in this PR or a separate one. Be sure to make any breaking changes clear in the next release notes.

@carbolymer carbolymer force-pushed the mgalazyn/refactor/move-wit-indexing-logic-into-one-place branch 2 times, most recently from 0d22df3 to 3f14ab0 Compare December 30, 2024 20:46
@carbolymer carbolymer force-pushed the mgalazyn/refactor/move-wit-indexing-logic-into-one-place branch from 3f14ab0 to c18d177 Compare December 31, 2024 17:36
Copy link
Contributor

@Jimbo4350 Jimbo4350 left a comment

Choose a reason for hiding this comment

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

LGTM! If you have a branch integrated in cardano-cli, I would ask Martin to run tests related to plutus scripts from the branch to be safe.

@carbolymer carbolymer added this pull request to the merge queue Jan 3, 2025
@carbolymer
Copy link
Contributor Author

Backported to 10.1 and tested in: https://github.com/IntersectMBO/cardano-node-tests/actions/runs/12596557732

Merged via the queue into master with commit a1a40fa Jan 3, 2025
70 checks passed
@carbolymer carbolymer deleted the mgalazyn/refactor/move-wit-indexing-logic-into-one-place branch January 3, 2025 12:36
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