Skip to content

Commit

Permalink
Implement burning
Browse files Browse the repository at this point in the history
  • Loading branch information
jhbertra authored and nhenin committed Mar 28, 2024
1 parent 52284c9 commit c87dc9c
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 29 deletions.
Expand Up @@ -390,7 +390,10 @@ instance Arbitrary BurnError where
frequency
[ (5, BurnRolesActive <$> arbitrary)
, (1, pure BurnNoTokens)
, (3, BurnBalancingError <$> arbitrary)
, (1, pure BurnFromCardanoError)
, (3, BurnConstraintError <$> arbitrary)
, (3, BurnEraUnsupported <$> arbitrary)
, (3, BurnInvalidPolicyId <$> arbitrary)
]
shrink = genericShrink

Expand Down
2 changes: 1 addition & 1 deletion marlowe-runtime/marlowe-runtime.cabal
Expand Up @@ -700,7 +700,7 @@ executable marlowe-tx
, hs-opentelemetry-sdk ^>=0.0.3
, marlowe-chain-sync ==0.0.6
, marlowe-protocols ==0.3.0.0
, marlowe-runtime:{marlowe-runtime, contract-api, tx, tx-api} ==0.0.6
, marlowe-runtime:{marlowe-runtime, contract-api, sync-api, tx, tx-api} ==0.0.6
, network >=3.1 && <4
, optparse-applicative ^>=0.16.1
, text ^>=2.0
Expand Down
3 changes: 3 additions & 0 deletions marlowe-runtime/marlowe-tx/Logging.hs
Expand Up @@ -7,6 +7,7 @@ module Logging (
) where

import Control.Monad.Event.Class (Inject (..))
import Language.Marlowe.Protocol.Query.Types (MarloweQuery)
import Language.Marlowe.Runtime.ChainSync.Api (
ChainSyncCommand,
ChainSyncQueryClientSelector,
Expand Down Expand Up @@ -43,6 +44,7 @@ data RootSelector f where
ChainSyncJobClient :: TcpClientSelector (Handshake (Job ChainSyncCommand)) f -> RootSelector f
ChainSyncQueryClient :: PeerT.TcpClientSelector ChainSyncQueryClientSelector f -> RootSelector f
ChainSeekClient :: PeerT.TcpClientSelector RuntimeChainSeekClientSelector f -> RootSelector f
MarloweQueryClient :: TcpClientSelector (Handshake MarloweQuery) f -> RootSelector f
ContractQueryClient :: TcpClientSelector (Handshake (Query ContractRequest)) f -> RootSelector f
Server :: TcpServerSelector (Handshake (Job MarloweTxCommand)) f -> RootSelector f
App :: TransactionServerSelector f -> RootSelector f
Expand Down Expand Up @@ -75,6 +77,7 @@ renderRootSelectorOTel = \case
ChainSyncQueryClient sel -> PeerT.renderTcpClientSelectorOTel renderChainSyncQueryClientSelector sel
ChainSeekClient sel -> PeerT.renderTcpClientSelectorOTel renderChainSeekClientSelectorOTel sel
ContractQueryClient sel -> renderTcpClientSelectorOTel sel
MarloweQueryClient sel -> renderTcpClientSelectorOTel sel
Server sel -> renderTcpServerSelectorOTel sel
App sel -> renderTransactionServerSelectorOTel sel
LoadWalletContext sel -> renderLoadWalletContextSelectorOTel sel
Expand Down
28 changes: 28 additions & 0 deletions marlowe-runtime/marlowe-tx/Main.hs
Expand Up @@ -14,6 +14,7 @@ import Control.Concurrent.Component.Run (AppM, runAppMTraced)
import qualified Data.Text as T
import Data.Time (NominalDiffTime)
import Data.Version (showVersion)
import Language.Marlowe.Protocol.Query.Client (MarloweQueryClient)
import Language.Marlowe.Runtime.ChainSync.Api (
BlockNo (..),
ChainSyncQuery (..),
Expand Down Expand Up @@ -114,6 +115,9 @@ run Options{..} = flip runComponent_ () proc _ -> do
contractQueryConnector :: Connector (QueryClient ContractRequest) (AppM Span RootSelector)
contractQueryConnector = tcpClientTraced (injectSelector ContractQueryClient) contractHost contractQueryPort queryClientPeer

marloweQueryConnector :: Connector MarloweQueryClient (AppM Span RootSelector)
marloweQueryConnector = tcpClientTraced (injectSelector MarloweQueryClient) syncHost marloweQueryPort queryClientPeer

MarloweTx{..} <-
transaction
-<
Expand Down Expand Up @@ -170,6 +174,8 @@ data Options = Options
, chainSeekHost :: HostName
, contractQueryPort :: PortNumber
, contractHost :: HostName
, marloweQueryPort :: PortNumber
, syncHost :: HostName
, port :: PortNumber
, host :: HostName
, submitConfirmationBlocks :: BlockNo
Expand All @@ -189,6 +195,8 @@ getOptions = execParser $ info (helper <*> versionOption <*> parser) infoMod
<*> chainSeekHostParser
<*> contractQueryPortParser
<*> contractHostParser
<*> marloweQueryPortParser
<*> syncHostParser
<*> portParser
<*> hostParser
<*> submitConfirmationBlocksParser
Expand Down Expand Up @@ -241,6 +249,16 @@ getOptions = execParser $ info (helper <*> versionOption <*> parser) infoMod
, showDefault
]

marloweQueryPortParser =
option auto $
mconcat
[ long "marlowe-query-port"
, value 3726
, metavar "PORT_NUMBER"
, help "The port number of the marlowe query server."
, showDefault
]

portParser =
option auto $
mconcat
Expand Down Expand Up @@ -271,6 +289,16 @@ getOptions = execParser $ info (helper <*> versionOption <*> parser) infoMod
, showDefault
]

syncHostParser =
strOption $
mconcat
[ long "sync-host"
, value "127.0.0.1"
, metavar "HOST_NAME"
, help "The host name of the marlowe sync server."
, showDefault
]

hostParser =
strOption $
mconcat
Expand Down
1 change: 1 addition & 0 deletions marlowe-runtime/runtime/Language/Marlowe/Runtime.hs
Expand Up @@ -179,6 +179,7 @@ marloweRuntime = proc MarloweRuntimeDependencies{..} -> do
let marloweBulkSyncServerSource = unnestServerSource $ MarloweSync.bulkSyncServerSource <$> mMarloweSync
let marloweBulkSyncConnector = directConnector serveMarloweBulkSyncClient marloweBulkSyncServerSource
let marloweQueryServerSource = unnestServerSource $ MarloweSync.queryServerSource <$> mMarloweSync
let marloweQueryConnector = directConnector serveQueryClient marloweQueryServerSource

mMarloweContract <- supervisor "marlowe-contract" contract -< ContractDependencies{..}

Expand Down
Expand Up @@ -814,7 +814,7 @@ instance Binary BurnTx where
_ -> fail $ "Invalid era tag value: " <> show eraTag

data BurnTxInEra era = BurnTxInEra
{ burnedTokens :: Chain.Assets
{ burnedTokens :: Chain.Tokens
, txBody :: TxBody era
}

Expand Down Expand Up @@ -843,9 +843,14 @@ data Account

data BurnError
= BurnEraUnsupported AnyCardanoEra
| BurnRolesActive (Set AssetId)
| BurnRolesActive (Set PolicyId)
| BurnInvalidPolicyId (Set PolicyId)
| BurnNoTokens
| BurnBalancingError String
| BurnFromCardanoError
| -- FIXME most of this error is not relevant to burning, but due to the current
-- constraint solving being too marlowe-specific, and because we use the
-- final balancing pipeline for burning, we sadly need to use this type here.
BurnConstraintError ConstraintError
deriving stock (Show, Eq, Ord, Generic)
deriving anyclass (Binary, ToJSON, Variations)

Expand Down
2 changes: 2 additions & 0 deletions marlowe-runtime/tx/Language/Marlowe/Runtime/Transaction.hs
Expand Up @@ -23,6 +23,7 @@ import Data.Maybe (catMaybes, mapMaybe, maybeToList)
import qualified Data.Set as Set
import Data.String (fromString)
import Data.Time (NominalDiffTime)
import Language.Marlowe.Protocol.Query.Client (MarloweQueryClient)
import Language.Marlowe.Runtime.ChainSync.Api (
ChainSyncQuery,
PlutusScript (..),
Expand Down Expand Up @@ -75,6 +76,7 @@ data TransactionDependencies m = TransactionDependencies
, getCurrentScripts :: forall v. MarloweVersion v -> MarloweScripts
, analysisTimeout :: NominalDiffTime
, mkRoleTokenMintingPolicy :: MkRoleTokenMintingPolicy m
, marloweQueryConnector :: Connector MarloweQueryClient m
}

data MarloweTx m = MarloweTx
Expand Down
206 changes: 199 additions & 7 deletions marlowe-runtime/tx/Language/Marlowe/Runtime/Transaction/Burn.hs
@@ -1,21 +1,213 @@
{-# LANGUAGE GADTs #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}
{-# OPTIONS_GHC -Wno-unused-matches #-}
{-# OPTIONS_GHC -Wno-unused-record-wildcards #-}

module Language.Marlowe.Runtime.Transaction.Burn where

import Cardano.Api (BabbageEraOnwards)
import Cardano.Api.Shelley (LedgerProtocolParameters)
import Control.Error (ExceptT)
import Language.Marlowe.Runtime.Transaction.Api (BurnError, BurnTxInEra, RoleTokenFilter)
import Language.Marlowe.Runtime.Transaction.Constraints (WalletContext (..))
import Cardano.Api (BabbageEraOnwards, BuildTx, ScriptInEra, SystemStart, TxBodyContent (..), defaultTxBodyContent)
import qualified Cardano.Api as C
import Cardano.Api.Shelley (LedgerProtocolParameters, PlutusScriptOrReferenceInput (..), ReferenceScript (..))
import Control.Error (ExceptT, note, throwE)
import Control.Monad (unless, when)
import Control.Monad.Trans.Class (MonadTrans (..))
import Control.Monad.Trans.Except (except)
import Data.Bifunctor (Bifunctor (..))
import Data.Coerce (coerce)
import Data.Foldable (Foldable (..))
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (mapMaybe)
import Data.Set (Set)
import qualified Data.Set as Set
import Language.Marlowe.Protocol.Query.Types (RoleCurrency (..))
import Language.Marlowe.Runtime.Cardano.Api (
toCardanoAddressInEra,
toCardanoLovelace,
toCardanoPolicyId,
toCardanoTxIn,
toCardanoTxOutValue,
tokensToCardanoValue,
)
import Language.Marlowe.Runtime.ChainSync.Api (
Address,
AssetId (..),
Assets (..),
ChainSyncQuery (..),
PolicyId (..),
ScriptHash (..),
Tokens (..),
TransactionOutput (..),
TxOutRef,
UTxOs (..),
)
import Language.Marlowe.Runtime.Core.Api (MarloweVersion (..))
import Language.Marlowe.Runtime.Transaction.Api (
BurnError (..),
BurnTxInEra (..),
RoleTokenFilter,
evalRoleTokenFilter,
)
import Language.Marlowe.Runtime.Transaction.Constraints (
HelpersContext (..),
PayoutContext (PayoutContext),
WalletContext (..),
adjustTxForMinUtxo,
balanceTx,
selectCoins,
)
import Network.Protocol.Connection (Connector, runConnector)
import Network.Protocol.Query.Client (QueryClient, request)
import UnliftIO (MonadUnliftIO)

burnRoleTokens
:: (MonadUnliftIO m)
=> BabbageEraOnwards era
=> SystemStart
-> C.EraHistory
-> Connector (QueryClient ChainSyncQuery) m
-> BabbageEraOnwards era
-> LedgerProtocolParameters era
-> WalletContext
-> Set RoleCurrency
-> RoleTokenFilter
-> ExceptT BurnError m (BurnTxInEra era)
burnRoleTokens era protocol WalletContext{..} tokenFilter = undefined
burnRoleTokens start history chainQueryConnector era protocol walletCtx@WalletContext{..} currencies tokenFilter = do
-- convert role currency info into a list
let currenciesList = Set.toList currencies
-- collect the policy IDs which are used by active contracts
let activeCurrencies = Set.fromList $ mapMaybe activeCurrency currenciesList
-- define a mapping of policyId to contracts which use them for role tokens.
let contractIdsByPolicyId = Map.fromListWith (<>) do
RoleCurrency{..} <- currenciesList
pure (rolePolicyId, Set.singleton roleContract)
-- Splits assets into ones which match the burn filter, and ones that don't.
let partitionAssets :: Map AssetId a -> (Map AssetId a, Map AssetId a)
partitionAssets = Map.partitionWithKey \token _ ->
any (flip (evalRoleTokenFilter tokenFilter) token) $
fold $
Map.lookup (policyId token) contractIdsByPolicyId
-- Processes a single output from the wallet's UTxO.
let processInput
:: TxOutRef
-> TransactionOutput
-> ( Map TxOutRef (Tokens, (Address, Assets))
, Set PolicyId
)
processInput txIn TransactionOutput{address, assets = assets@(Assets lovelace (Tokens tokens))} =
case partitionAssets tokens of
(toBurn, toKeep)
| Map.null toBurn -> mempty
| otherwise ->
( Map.singleton txIn (Tokens toBurn, (address, Assets lovelace $ Tokens toKeep))
, Set.intersection activeCurrencies $ Set.map policyId $ Map.keysSet toBurn
)
-- Fold over the wallet's UTxO, selecting outputs to use as transaction inputs and looking for any
-- matching currencies which are still active.
let (inputs, activeOwnedCurrencies) = Map.foldMapWithKey processInput $ unUTxOs availableUtxos
-- If the burn includes active role tokens, abort
unless (Set.null activeOwnedCurrencies) $ throwE $ BurnRolesActive activeOwnedCurrencies
-- If the burn is empty, abort
when (Map.null inputs) $ throwE BurnNoTokens
-- Fetch all the minting scripts needed to burn the tokens.
let policyScriptHashes = foldMap (scriptHashesFromTokens . fst) inputs
scripts <- lift $ runConnector chainQueryConnector $ request $ GetScripts era policyScriptHashes
-- If there are policies for which scripts can't be found, abort.
let missingScriptHashes = Set.difference policyScriptHashes $ Map.keysSet scripts
unless (Set.null missingScriptHashes) $ throwE $ BurnRolesActive $ Set.mapMonotonic coerce missingScriptHashes
-- Build the transaction body
txBodyContent <- except $ note BurnFromCardanoError $ buildBurn era inputs scripts
-- FIXME there is no reason we need these except that selectCoins and balanceTx require them. Refactor
-- those two functions to remove these dummy contexts.
let scriptCtx = Right $ PayoutContext mempty mempty
let helpersCtx =
HelpersContext
{ currentHelperScripts = mempty
, helperPolicyId = ""
, helperScriptStates = mempty
}
txBody <-
except $
first BurnConstraintError $
adjustTxForMinUtxo era protocol Nothing txBodyContent
>>= selectCoins era protocol MarloweV1 scriptCtx walletCtx helpersCtx
>>= balanceTx era start (C.toLedgerEpochInfo history) protocol MarloweV1 scriptCtx walletCtx helpersCtx
let burnedTokens = foldMap fst inputs
pure BurnTxInEra{..}

scriptHashesFromTokens :: Tokens -> Set ScriptHash
scriptHashesFromTokens = Set.map (ScriptHash . unPolicyId . policyId) . Map.keysSet . unTokens

assetsFromUtxos :: UTxOs -> Assets
assetsFromUtxos = foldMap assets . unUTxOs

activeCurrency :: RoleCurrency -> Maybe PolicyId
activeCurrency RoleCurrency{..}
| active = Just rolePolicyId
| otherwise = Nothing

buildBurn
:: forall era
. BabbageEraOnwards era
-> Map TxOutRef (Tokens, (Address, Assets))
-> Map ScriptHash (ScriptInEra era)
-> Maybe (TxBodyContent BuildTx era)
buildBurn era inputs scripts = do
txIns <- traverse buildInput $ Map.keys inputs
(outputsWithTokens, adaOnlyOutputs) <- fold <$> traverse (uncurry buildOutput . snd) inputs
let txOuts = mergeAdaOnly adaOnlyOutputs <> outputsWithTokens
txMintValue <- buildMint
pure (defaultTxBodyContent shelleyEra){txIns, txOuts, txMintValue}
where
shelleyEra :: C.ShelleyBasedEra era
shelleyEra = C.babbageEraOnwardsToShelleyBasedEra era

maryEraOnwards :: C.MaryEraOnwards era
maryEraOnwards = C.babbageEraOnwardsToMaryEraOnwards era

buildInput :: TxOutRef -> Maybe (C.TxIn, C.BuildTxWith BuildTx (C.Witness C.WitCtxTxIn era))
buildInput = fmap (,C.BuildTxWith $ C.KeyWitness C.KeyWitnessForSpending) . toCardanoTxIn

buildOutput :: Address -> Assets -> Maybe ([C.TxOut C.CtxTx era], [(C.AddressInEra era, C.Lovelace)])
buildOutput address assets@(Assets lovelace (Tokens tokens)) = do
address' <- toCardanoAddressInEra (C.babbageEraOnwardsToCardanoEra era) address
let lovelace' = toCardanoLovelace lovelace
if Map.null tokens
then pure ([], [(address', lovelace')])
else do
value <- toCardanoTxOutValue maryEraOnwards assets
pure ([C.TxOut address' value C.TxOutDatumNone ReferenceScriptNone], [])

mergeAdaOnly :: [(C.AddressInEra era, C.Lovelace)] -> [C.TxOut C.CtxTx era]
mergeAdaOnly = fmap (uncurry buildAdaOnlyOutput) . Map.toList . Map.fromListWith (<>)

buildAdaOnlyOutput :: C.AddressInEra era -> C.Lovelace -> C.TxOut C.CtxTx era
buildAdaOnlyOutput address lovelace =
C.TxOut address (C.lovelaceToTxOutValue shelleyEra lovelace) C.TxOutDatumNone ReferenceScriptNone

buildMint :: Maybe (C.TxMintValue BuildTx era)
buildMint = do
(value, witnesses) <- fold <$> traverse (buildAssetMint . fst) inputs
pure $ C.TxMintValue maryEraOnwards (C.negateValue value) (C.BuildTxWith witnesses)

buildAssetMint :: Tokens -> Maybe (C.Value, Map C.PolicyId (C.ScriptWitness C.WitCtxMint era))
buildAssetMint tokens@(Tokens tokenMap) = do
value <- tokensToCardanoValue tokens
witnesses <- fold <$> traverse buildMintWitness (Map.keys $ Map.mapKeys policyId tokenMap)
pure (value, witnesses)

buildMintWitness :: PolicyId -> Maybe (Map C.PolicyId (C.ScriptWitness C.WitCtxMint era))
buildMintWitness policyId = do
policyId' <- toCardanoPolicyId policyId
C.ScriptInEra lang script <- Map.lookup (coerce policyId) scripts
witness <- case script of
C.PlutusScript v script' ->
pure $
C.PlutusScriptWitness
lang
v
(PScript script')
C.NoScriptDatumForMint
(C.unsafeHashableScriptData $ C.ScriptDataConstructor 1 []) -- This corresponds to the Burn action in the validator.
(C.ExecutionUnits 0 0)
_ -> Nothing
pure $ Map.singleton policyId' witness

0 comments on commit c87dc9c

Please sign in to comment.