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

Transaction builder #34

Merged
merged 47 commits into from
Aug 10, 2024
Merged

Transaction builder #34

merged 47 commits into from
Aug 10, 2024

Conversation

mpizenberg
Copy link
Owner

@mpizenberg mpizenberg commented Jul 28, 2024

This PR is a WIP, but ready for a first pass of reviews.

Here is a screenshot to have a quick overview of the API without having to look at the changes or using elm-doc-preview. (Might not be up-to-date with the latest state of this PR)

elm-cardano-txbuilding-2024-07-29

I’ve started by drafting all the types and writing the documentation, so that reading the documentation should tell you exactly how the building would be done in a majority of cases. I’ve also added at the end of the files some non-exposed examples in code, not only in documentation, so that the compiler would check that it makes sense.

The base idea is that there is a Tx status type, that should contain all necessary information while building the transaction. The status type parameter is a phantom type to record progress in the building and impose some very light constraints. I could have gone more type-heavy with status to have even more precise constraints, but I don’t think it’s worth it before having first some feedback.

Basically, the construction of the Tx goes as follows:

initTx
  --> Tx WIP
  |> addSomeTransfers
  |> addSomeMintsBurns
  |> addSomeScriptStuff
  |> handleChange someChangeStrat
  --> Tx AlmostReady
  |> addSomeMetadata
  |> addSomeTimeConstraints
  |> addFeeStrategy
  |> finalizeTx withSomeConfig
  --> Result String Transaction (or Task of some sort, ...)

So basically, we have the following steps:

  • Initialization
  • Add some intents
  • Handle change
  • Add some optional stuff
  • Finalize the Tx

Nothing is implemented, there are basically Debug.todo everywhere, but it typechecks. Also, the finalization step I haven’t thought too much about yet. Wondering if that can stay a pure function, or eventually return a Task of some sort, that will then be sent for signature.

@mpizenberg
Copy link
Owner Author

To view the generated documentation, you can install elm-doc-preview and run it from the project home.

@mpizenberg
Copy link
Owner Author

mpizenberg commented Jul 29, 2024

An alternative way of building might be the following:

type alias Tx =
  { intents : List TxIntent
  , other : List TxOther
  }

finalizeTx : List Tx -> ChangeStrategy -> OtherLocalContextStuff -> Result String Transaction

type alias ChangeStrategy = List ( Output, Value ) -> ChangeReallocation

Where TxIntent would basically be the transfer|mint|plutus|... stuff, and TxOther would be the fee|time|metadata|... stuff.

Then I think it would be easier to build a Tx in small parts and join them together. Something like this?

nftMintTx =
  { intents = [ mintNft, transferToMe ]
  , other = [ nftMetadata ]
  }

swapTx =
  { intents = [ transferFromMe, sendToScript ]
  , other = [ swapMetadata ]
  }

finalizeTx [ nftMintTx, swapTx ] myChangeStrat otherContextStuff

No idea if that would be any better, or even more verbose and annoying ...

@mpizenberg
Copy link
Owner Author

One key design decision in this PR is the handleChange function, that is required and only called once. I did this because I thought it would enable better solutions for the transaction building problem/algorithm. Indeed, when handling intents change one-at-a-time, we do not have the full picture yet.

For example, we might add a transfer, and say to send the change back to us, but while doing something else (e.g. a swap), the change in that utxo might have been useful to use instead of sending it back and having to look for another utxo for input for the swap.

That being said, it made the last example (spending half of a utxo in a script) more challenging because we want to send the rest of that utxo back to the same script address with the same metadata. In practice, I knew the amount that had to be sent, so I could easily build the change to send back 1 ada to the contract. But what if I wanted to have it automatically figured out? I’d have to look through the whole list of consumed outputs to find the one of the script, get that amount and use it. It’s doable, just adds much more verbosity, meaning potentially bugs?

I’m wondering if a better way, would be to add "partial change" handling in the spending intents, especially the one from plutus script. Something like this:

spendFromPlutusScript :
    PlutusScriptSource
    -> ScriptUtxoSelection
    -> Value
    -> ChangeStrat -- new
    -> Tx WIP
    -> Tx WIP

Or maybe, I suspect this will only be useful if you manually select the spent utxos in the first place, but I don’t know. In that case, it could be only an addition to the manual selection type:

type ScriptUtxoSelection
    = AutoScriptUtxoSelection ({ ref : OutputReference, utxo : Output } -> Maybe { redeemer : Data })
    | ManualScriptUtxoSelection
        { selection :
            List
                { ref : OutputReference
                , utxo : Output
                , redeemer : Data
                }
        , partialChange : ChangeStrat -- new
        }

No idea if that’s better in the grand scheme of composing transactions.

@mpizenberg
Copy link
Owner Author

Another remark. I want to be able to accommodate a design pattern for composable transactions that rely on the position/ordering of inputs, with markers provided by the redeemer. A more complete explanation of what I mean is given by comments cardano-foundation/CIPs#758 (comment) by @colll78 and cardano-foundation/CIPs#758 (comment) by @fallen-icarus. So maybe I need to look a bit more into these contracts. Adding markers for spent script inputs that can be used in redeemers data seems like a relevant use case.

@mpizenberg
Copy link
Owner Author

Also another reference to keep in mind: mlab’s new purescript transaction builder https://github.com/mlabs-haskell/purescript-cardano-transaction-builder

@klntsky
Copy link

klntsky commented Jul 30, 2024

@mpizenberg consider https://github.com/klntsky/cardano-purescript altogether. the builder is useless by itself, as it is just a small dsl+interpreter

@mpizenberg
Copy link
Owner Author

thanks @klntsky . From what I read, the main missing part from the builder dsl is the balancing part which lives in https://github.com/Plutonomicon/cardano-transaction-lib right? I don’t have time to look at all the links in your awesome list ^^

@klntsky
Copy link

klntsky commented Jul 30, 2024

@mpizenberg yes

@mpizenberg
Copy link
Owner Author

@klntsky I didn’t find a way with purescript-cardano-transaction-builder to build transactions following indexing patterns, like the one-to-one input/output indexer in the redeemer pattern described in anastasia labs readme here: https://github.com/Anastasia-Labs/aiken-design-patterns?tab=readme-ov-file#singular-utxo-indexer

Do you have an idea of how that would be possible?

@mpizenberg
Copy link
Owner Author

Another relevant source used by Anastasia Labs https://github.com/j-mueller/sc-tools/blob/main/src/base/lib/Convex/BuildTx.hs

@klntsky
Copy link

klntsky commented Jul 30, 2024

I didn’t find a way with purescript-cardano-transaction-builder to build transactions following indexing patterns

If control over indices is needed, best to build the transaction in plain PS

@colll78
Copy link

colll78 commented Jul 31, 2024

It's not just control over the indexes that is needed, it is the ability to construct and use redeemers that depend on the indices of inputs (which can change at balancing). The redeemer should accurately reflect their correct indices as they appear in the final tx (after balancing).

@mpizenberg
Copy link
Owner Author

mpizenberg commented Jul 31, 2024

So I’ve discussed a bit with Matthias and it seems like a function-heavy API is a bit too intense, so probably a DSL approach is easier to grasp. The change control also seems mentally taxing. I liked that it enabled making sure the user is building a balanced transaction though. So I’ll probably move to a balancing approach like most others, but still not 100% sold on this. Anyway.

Taking some feedback into account, it could make sense to move to something very similar to purescript DSL approach, but with two key differences.

(1) Enabling intents based on amounts and addresses, which avoids having to select exact output references or create exact outputs for things that are fungible (so mainly assets at regular addresses or native script addresses).

(2) Enabling redeemer construction with a function that depends on the selected inputs and created outputs. Finalizing the Tx will require a multi-pass solver anyway, so I see no harm in helping dynamic redeemer constructions, as long as we protect from infinite loops in the solver.

Here is what it would look like (only describing the Spend intent as its the one relevant to redeemers I’m lazy and we can do the same with other purposes).

type Intent
    = Spend SpendSource
    | ...

type SpendSource
    = AutoSelectFrom Address Value
    | FromUtxo { input : OutputReference, spendWitness : SpendWitness }

type SpendWitness
    = NoSpendWitness
    | NativeWitness (ScriptWitness NativeScript)
    | PlutusWitness
        { scriptWitness : ScriptWitness PlutusScript
        , datumWitness : DatumWitness
        , redeemerDatum : RedeemerDatum
        }

type RedeemerDatum
    = FixedRedeemer Data
    -- This should enable easier indexing pattern
    | RedeemerFunction (RedeemerContext -> Data)

type alias RedeemerContext =
    { referenceInputs : List OutputReference
    , spentInputs : List OutputReference
    , createdOutputs : List Output
    }

@keyan-m
Copy link
Contributor

keyan-m commented Jul 31, 2024

Here is what it would look like (only describing the Spend intent as its the one relevant to redeemers I’m lazy and we can do the same with other purposes).

In this new API, including required signers under ScriptWitness might be a better interface than addRequiredSigners.

@mpizenberg
Copy link
Owner Author

mpizenberg commented Jul 31, 2024

Ok, I’ve refined it a bit more. Still very close to the purescript DSL but slightly different.

type TxIntent
    = SendToAutoCreate Address Value
    | SendToOutput (InputsOutputs -> Output)
      -- Spending assets from somewhere
    | SpendFromAutoSelect Address Value
    | SpendFromUtxo
        { input : OutputReference
        , spendWitness : SpendWitness
        }
      -- Minting / burning assets
    | MintBurn
        { policyId : Bytes CredentialHash
        , assets : BytesMap AssetName Integer
        , credentialWitness : CredentialWitness
        }
      -- Issuing certificates
    | IssueCertificate Todo
      -- Withdrawing rewards
    | WithdrawRewards Todo

type alias InputsOutputs =
    { referenceInputs : List OutputReference
    , spentInputs : List OutputReference
    , createdOutputs : List Output
    }

type CredentialWitness
    = NativeScriptCredential (ScriptWitness NativeScript)
    | PlutusScriptCredential
        { scriptWitness : ScriptWitness PlutusScript
        , redeemerData : InputsOutputs -> Data
        }

type SpendWitness
    = NoSpendWitness
    | NativeWitness (ScriptWitness NativeScript)
    | PlutusWitness
        { scriptWitness : ScriptWitness PlutusScript
        , datumWitness : Maybe DatumWitness
        , redeemerData : InputsOutputs -> Data
        }

type ScriptWitness a
    = ScriptValue a
    | ScriptReference OutputReference

type DatumWitness
    = DatumValue Data
    | DatumReference OutputReference

@mpizenberg
Copy link
Owner Author

In this new API, including required signers under ScriptWitness might be a better interface than addRequiredSigners.

@keyan-m something like this:

type ScriptWitness a
    = ScriptValue
        { script : a
        , requiredSigners : List (Bytes CredentialHash)
        }
    | ScriptReference
        { ref : OutputReference
        , requiredSigners : List (Bytes CredentialHash)
        }

@mpizenberg
Copy link
Owner Author

Or maybe better, I add it the plutus variants of the credential and spend witnesses to avoid the incorrect state of having required signers for a native script.

type CredentialWitness
    = NativeScriptCredential (ScriptWitness NativeScript)
    | PlutusScriptCredential
        { scriptWitness : ScriptWitness PlutusScript
        , redeemerData : InputsOutputs -> Data
        , requiredSigners : List (Bytes CredentialHash) -- new
        }

type SpendWitness
    = NoSpendWitness
    | NativeWitness (ScriptWitness NativeScript)
    | PlutusWitness
        { scriptWitness : ScriptWitness PlutusScript
        , datumWitness : Maybe DatumWitness
        , redeemerData : InputsOutputs -> Data
        , requiredSigners : List (Bytes CredentialHash) -- new
        }

@mpizenberg
Copy link
Owner Author

mpizenberg commented Aug 10, 2024

Ok, I feel like this PR has gone far enough to be merged, even if incomplete. The current state of Tx building is the following.

The building API has converged to a DSL approach, with two specificities. (1) There is a Spend <| From address value variant and a SendTo address value variant that enable loose specification of inputs and outputs, that can be leveraged by the coin selection algorithm. (2) Redeemers are specified via a function of the shape InputsOutputs -> ... Redeemer ... enabling redeemer values to be constructed by inspecting the list of inputs and outputs. This is especially useful for patterns like the UTxO indexing described in Anastasia Lab’s aiken patterns repo.

This PR also provides a first implementation of the Tx builder finalization step. That step processes all intents, performs coin selection and build a balanced Transaction. Current limitations include the fact that Tx fees and script costs are not estimated yet, so the balancing is done without. But it would not fundamentally change how the current code works so this doesn’t contradict this proof of concept. The main difference in the future follow up PR, is that it will need to define side effects and ports communication to finalize the Tx with correct fees and script costs.

Another limitation is that this PR does not perform blake2b hashing yet for the different fields that require them.

Finally, this PR includes some example code in the bottom of the Cardano module, as well as in a dedicated examples/txbuild/ folder. It temporarily exposes some values, for testing purposes that will eventually be removed or moved elsewhere.

That’s all :)

Enjoy this screenshot from the txbuild example.

image

@mpizenberg mpizenberg marked this pull request as ready for review August 10, 2024 16:11
@mpizenberg mpizenberg merged commit 2f29e31 into main Aug 10, 2024
4 of 5 checks passed
@mpizenberg mpizenberg deleted the txbuild branch August 10, 2024 16:12
@mpizenberg mpizenberg mentioned this pull request Aug 11, 2024
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.

4 participants