Architecture for "Marlowe Transaction Creation" #213
Replies: 14 comments 31 replies
-
Sequence of InteractionsTransaction to Apply Inputs
Transaction to Create Contract
Transaction to Withdraw Funds
|
Beta Was this translation helpful? Give feedback.
-
Because we might want to defer some Transaction Creation functions to wallets, we should aim for very clean separation between sub-components:
|
Beta Was this translation helpful? Give feedback.
-
Here is work in progress on a proof-of-principle example for coin selection and transaction creation in Marlowe: #240. |
Beta Was this translation helpful? Give feedback.
-
@bwbush thanks for this thorough summary of the requirements! This very clearly lays out everything that needs to be done to create the transactions. The first thing that comes to me is that there are potentially two approaches we can take with this component, each of which offers advantages and disadvantages:
Advantages of the pure pipeline approach:
Advantages of the protocol approach:
My intuition tells me that having a pure function to compute the unsigned transaction given a full universe of inputs would be extremely useful, but I also think the interactive, incremental approach of being able to request resources as needed from the client has advantages that would be a shame to lose. Ultimately, I think we may be able to have it both ways. If we implement the transaction creation as a protocol, we can convert that to a pure function which accepts a context data structure that contains all the necessary information, and feeds that into the protocol. This could be used as a library function, or it could be used by the CLI when working with a fixed set of addresses. The protocol version could be used when writing a wallet integration, or in an interactive CLI mode. |
Beta Was this translation helpful? Give feedback.
-
For this component, I think it's worth discussing what the CLI might look like, as that could affect some of the architecture choices we make. Here's one possible API: Contract Commands
Create Command
Apply Inputs Command
Deposit Command
Choose Command
Notify Command
Submit Command
Withdraw Command
|
Beta Was this translation helpful? Give feedback.
-
@jhbertra, @paluh, @dino-iog: here is an complete implementation of a coin selection algorithm for Marlowe transactions. The code is not tidy, but it is fully documented and provides a recipe that could be refactored into a clean implementation: marlowe-cardano/marlowe-cli/src/Language/Marlowe/CLI/Transaction.hs Lines 1059 to 1185 in cd66f37 Here are closely related artifacts:
|
Beta Was this translation helpful? Give feedback.
-
Sketch for signature of coin-selection pure function, in terms of selectCoins :: UTxO -- ^ The inputs that the transaction must spend.
-> [TxOut CtxTx era] -- ^ The outputs that the transaction must produce.
-> UTxO -- ^ The additional inputs that may be spent by coin selection.
-> Lovelace -- ^ The minimum required excess Lovelace for fee and change.
-> (TxOut CtxTx era -> TxOut CtxTx era) -- ^ Function that adjusts an output to conform to the minimum-UTxO ledger rule.
-> Either
e -- ^ Report an error.
(
Maybe TxIn -- ^ Suitable collateral. (This would be more complex for Babbage.)
, [TxIn] -- ^ The inputs selected for spending, along with the required ones.
, [TxOut CtxTx era] -- ^ The outputs of the transaction, both selected and required.
, Lovelace -- ^ The excess Lovelace for fee and change: i.e., the difference between spending and production.
) |
Beta Was this translation helpful? Give feedback.
-
The signature for balancing is the same as marlowe-cardano/marlowe-cli/src/Language/Marlowe/CLI/Transaction.hs Lines 669 to 715 in 8eaac78 |
Beta Was this translation helpful? Give feedback.
-
I'm trying to organize my thoughts around these requirements and thought I'd lay some of them out here.
-- Correspond to Step 2 above
buildCreateConstraints :: CreateContractRequest -> Either CreateContractError TransactionConstraints
buildApplyInputsConstraints :: ApplyInputsRequest -> Either ApplyInputsError TransactionConstraints
buildWithdrawConstraints :: WithdrawRequest -> Either WithdrawError TransactionConstraints
-- Corresponds to step 3 above
selectCoins :: TransactionConstraints -> Either SelectCoinsError TransactionConstraints
-- Corresponds to step 4 above
balanceTransaction :: TransactionConstraints -> Either BalanceTransactionError TransactionConstraints
-- Corresponds to step 5 above
buildTransaction :: ScriptDataSupportedInEra era -> TransactionConstraints -> Either BuildTransactionError (Cardano.Api.TxBody era)
-- Corresponds to step 6 above
signTransaction :: Cardano.Api.TxBody era -> m (Either SignTransactionError (Cardano.Api.TxBody era))
-- How to best represent step 7? In here, the data TransactionConstraints = TransactionConstraints
{ availableUtxo :: UTxO
, outputs :: [TxOut]
, spentUtxo :: UTxO
, collateral :: Maybe TxIn
} Or something like that. It carries both the available UTxO and the UTxO which will be consumed as input. Each stage would update this structure (we could have it as an opaque type with invariants that should always be preserved - e.g. there are no unary operations that change the total UTxO value (you can shuffle tx outs from available to spent, but neither create nor destroy them). |
Beta Was this translation helpful? Give feedback.
-
Some ideas for the wallet interface:
|
Beta Was this translation helpful? Give feedback.
-
From an architectural perspective, I'm starting to think the runtime component should take the protocol-based approach mentioned above, with the client filling the roles of A) providing wallet addresses and B) delegating transaction signing. This decouples the wallet from the runtime very effectively, allowing consumers to plug in the wallet backend of their choice. From the perspective of the CLI, providing explicit signing key files (like the I arrived at this point of view by imagining things from the perspective of a 3rd party app using the runtime: if I want to be able to integrate the wallet of my choice into my DApp, I don't want to be limited by the fact that I'm using the Marlowe Runtime and have to go through it to integrate the wallet. Instead, I'd want to be able to provide the wallet data the runtime asks for by querying it from the wallet myself. |
Beta Was this translation helpful? Give feedback.
-
I was trying to create a sequence diagram to represent the various interactions between components and quickly realized that having this as a long-running stateful protocol was going to result in a lot of unnecessary complexity. Instead, I've modelled the core transaction submission API as a set of commands. This is functionally equivalent to the protocol approach, except that the state is represented via the intermediate arguments and results, making the runtime process stateless. The idea is to expose these commands via a -- | The low-level runtime API for building and submitting transactions.
data MarloweTxCommand status err result where
-- | Construct a transaction that starts a new Marlowe contract. The
-- resulting, unsigned transaction can be signed via the cardano API or a
-- wallet provider. When signed, the 'Submit' command can be used to submit
-- the transaction to the attached Cardano node.
Create
:: IsShelleyBasedEra era
=> MarloweVersion v
-- ^ The Marlowe version to use
-> WalletAddresses
-- ^ The wallet addresses to use when constructing the transaction
-> Map TokenName Address
-- ^ The initial distribution of role tokens
-> Map Int Aeson.Value
-- ^ Optional metadata to attach to the transaction
-> Contract v
-- ^ The contract to run
-> MarloweTxCommand Void CreateError
( ContractId -- The ID of the contract (tx output that carries the datum)
, TxBody era -- The unsigned tx body, to be signed by a wallet.
)
-- | Construct a transaction that advances an active Marlowe contract by
-- applying a sequence of inputs. The resulting, unsigned transaction can be
-- signed via the cardano API or a wallet provider. When signed, the 'Submit'
-- command can be used to submit the transaction to the attached Cardano node.
ApplyInputs
:: IsShelleyBasedEra era
=> MarloweVersion v
-- ^ The Marlowe version to use
-> WalletAddresses
-- ^ The wallet addresses to use when constructing the transaction
-> ContractId
-- ^ The ID of the contract to apply the inputs to.
-> Maybe UTCTime
-- ^ The "invalid before" bound of the validity interval. If omitted, this
-- is computed from the contract.
-> Maybe UTCTime
-- ^ The "invalid hereafter" bound of the validity interval. If omitted, this
-- is computed from the contract.
-> Core.Redeemer v
-- ^ The inputs to apply.
-> MarloweTxCommand Void ApplyInputsError
( TxBody era -- The unsigned tx body, to be signed by a wallet.
)
-- | Construct a transaction that withdraws available assets from an active
-- Marlowe contract for a set of roles in the contract. The resulting,
-- unsigned transaction can be signed via the cardano API or a wallet
-- provider. When signed, the 'Submit' command can be used to submit the
-- transaction to the attached Cardano node.
Withdraw
:: IsShelleyBasedEra era
=> MarloweVersion v
-- ^ The Marlowe version to use
-> WalletAddresses
-- ^ The wallet addresses to use when constructing the transaction
-> ContractId
-- ^ The ID of the contract to withdraw assets from.
-> Set TokenName
-- ^ The names of the roles whose assets to withdraw.
-> MarloweTxCommand Void WithdrawError
( TxBody era -- The unsigned tx body, to be signed by a wallet.
)
-- | Submits a signed transaction to the attached Cardano node.
--
-- NOTE While the intended use for this command is to submit the transactions
-- created with the other commands in this API, it is in fact a
-- general-purpose API for submitting any signed transaction to the node via
-- the Runtime... such use would technically be abuse of the Runtime, as
-- such, I am wondering if this opens up potential vulnerabilities.
Submit
:: IsShelleyBasedEra era
=> Tx era
-- ^ A signed transaction to submit
-> MarloweTxCommand
SubmitStatus -- This job reports the status of the tx submission, which can take some time.
SubmitError
BlockHeader -- The block header of the block this transaction was added to.
data WalletAddresses = WalletAddresses
{ changeAddress :: Address
, extraAddresses :: Address
}
data CreateError
data ApplyInputsError
data WithdrawError
data SubmitError
data SubmitStatus
= Submitting
| Submitted
| Accepted
|
Beta Was this translation helpful? Give feedback.
-
I'm clarifying that in data TransactionConstraints = TransactionConstraints
{ availableUtxos :: Map TxOutRef TransactionOutput
, mustProduceOutputs :: [TransactionOutput]
, mustConsumeInputs :: Set (TxOutRef, Maybe Redeemer)
, mustMintTokens :: Tokens
, mustSendCollateralInputs :: Set TxOutRef
, mustReturnCollateralOutputs :: [TransactionOutput]
}
|
Beta Was this translation helpful? Give feedback.
-
Here's a new version of module Language.Marlowe.Runtime.Transaction.Constraints where
import Cardano.Api (AddressInEra, EraHistory, IsShelleyBasedEra, ScriptDataSupportedInEra, TxBody)
import Cardano.Api.Byron (EraInMode)
import Cardano.Api.Shelley (PoolId, ProtocolParameters)
import qualified Data.Aeson as Aeson
import Data.Function (on)
import Data.Map (Map)
import Data.Set (Set)
import qualified Data.Set as Set
import Language.Marlowe.Runtime.ChainSync.Api
data TransactionConstraints = TransactionConstraints
{ requiredOutputs :: [TransactionOutput]
, requiredInputs :: Set (TxOutRef, Maybe Redeemer)
, mints :: Tokens
, requiredMetadata :: Map Int Aeson.Value
, requiredSignatures :: Set PaymentKeyHash
}
instance Semigroup TransactionConstraints where
a <> b = TransactionConstraints
{ requiredOutputs = on (<>) requiredOutputs a b
, requiredInputs = on (<>) requiredInputs a b
, mints = on (<>) mints a b
, requiredMetadata = on (<>) requiredMetadata a b
, requiredSignatures = on (<>) requiredSignatures a b
}
instance Monoid TransactionConstraints where
mempty = TransactionConstraints
{ requiredOutputs = mempty
, requiredInputs = mempty
, mints = mempty
, requiredMetadata = mempty
, requiredSignatures = mempty
}
-- | The resulting transaction will use the given UTXO as an input.
mustSpendUtxo :: TxOutRef -> Maybe Redeemer -> TransactionConstraints
mustSpendUtxo txOutRef redeemer = mempty { requiredInputs = Set.singleton (txOutRef, redeemer) }
-- | The resulting transaction will produce the given output as an input. Order
-- of outputs is determined by the order in which constraints are appended.
mustProduceOutput :: TransactionOutput -> TransactionConstraints
mustProduceOutput output = mempty { requiredOutputs = [output] }
-- | The resulting transaction will mint the specified tokens.
mustMintTokens :: Tokens -> TransactionConstraints
mustMintTokens mints = mempty { mints = mints }
-- | The resulting transaction will include the specified metadata at the
-- specified index.
mustIncludeMetadata :: Map Int Aeson.Value -> TransactionConstraints
mustIncludeMetadata metadata = mempty { requiredMetadata = metadata }
-- | The resulting transaction requires a signature for the given payment key hash.
mustBeSignedBy :: PaymentKeyHash -> TransactionConstraints
mustBeSignedBy paymentKey = mempty { requiredSignatures = Set.singleton paymentKey }
data WalletContents era = WalletContents
{ utxos :: Map TxOutRef TransactionOutput -- ^ All available UTXOs for the wallet
, collateralUtxos :: Set TxOutRef -- ^ The set of UTXOs which can be used as collateral
, changeAddress :: AddressInEra era -- ^ The wallet's change address for sending surplus balance.
}
-- | Solve the given transaction constraints such that the resulting transaction is balanced.
-- This is to say that feeding the resulting 'TxBody' back into 'Cardano.Api.makeTransactionBodyAutoBalance'
-- would produce a 'BalancedTxBody' in which the change output is zero.
solveConstraints
:: forall era mode
. ScriptDataSupportedInEra era
-> EraInMode era mode
-> SystemStart
-> EraHistory mode
-> ProtocolParameters
-> WalletContents era
-> TransactionConstraints
-> Either SolveConstraintsError (TxBody era)
solveConstraints = _ |
Beta Was this translation helpful? Give feedback.
-
The Marlowe Transaction Creation component of Marlowe Runtime builds a valid Marlowe transaction from the inputs that will be applied to the contract, the constraints on Marlowe transactions in general, and the UTxOs available for building the transaction.
Resources:
Acceptance criteria:
Beta Was this translation helpful? Give feedback.
All reactions