Marlowe History Architecture discussion #201
Replies: 11 comments 42 replies
-
presumably "either rolls the client forward or backward" |
Beta Was this translation helpful? Give feedback.
-
Should we account for the edge case that one transaction creates two Marlowe contracts? |
Beta Was this translation helpful? Give feedback.
-
Will |
Beta Was this translation helpful? Give feedback.
-
We'll need to support the following query patterns:
What else? See also #180 (comment). |
Beta Was this translation helpful? Give feedback.
-
Contract filter considerationsPotential requirements for contract filtering
Specifying Filters in the RuntimeHere are some options for how filters could be managed
Design considerationsSince traversing the chain is expensive, we may want to consider over fetching contract IDs and associating them with filter attributes. There could be a chain seek client which watches for all contract creation transactions and extracts info like: what is the role token currency for the contract? What are the pub key hashes / addresses mentioned in the contract? We could store this info in a way that is easy to query by the filter attributes, which would avoid having to re-traverse the chain from Genesis each time the filter changed. |
Beta Was this translation helpful? Give feedback.
-
Data type ideas for the event-oriented representation of Marlowe contracts: newtype ContractEvents = ContractEvents { unContractEvents :: Map BlockHeader (Map ContractId [ContractEvent]) }
newtype ContractId = ContractId { unContractId :: TxOutRef } -- The TxOut that created the contract.
newtype PayoutId = PayoutId { unPayoutId :: TxOutRef } -- The TxOut that transferred the payout.
data ContractEvent
= Created
{ roleTokenPolicyId :: PolicyId
, marloweValidatorHash :: ValidatorHash
, payoutValidatorHash :: ValidatorHash
, contract :: Contract
}
| InputsApplied
{ inputs :: [Input]
, outputs :: Outputs
}
| TimeoutElapsed
| PayoutRedeemed
{ payoutId :: PayoutId
}
data Party
= Address Address
| Role TokenName
newtype AccountId = AccountId Party
data Input
= Deposit
{ fromParty :: Party
, toAccount :: AccountId
, assets :: Assets
, continuation :: Maybe ContractContinuation
}
| Choice
{ choiceId :: ChoiceId
, choiceValue :: ChoiceValue
, continuation :: Maybe ContractContinuation
}
data Outputs = Outputs
{ nextUTxO :: Maybe TxOutRef
, payments :: [Payment]
, warnings :: [TransactionWarning]
}
data Payment = Payment
{ fromAccount :: AccountId
, toRecipient :: Recipient
, assets :: Assets
}
data Recipient
= Account AccountId
| Party PaymentId Party
data ContractContinuation = ContractContinuation
{ hash :: ContractHash
, contract :: Contract
} |
Beta Was this translation helpful? Give feedback.
-
I have been thinking some more about this filtering problem, and I realized that perhaps we're giving Marlowe History a bit too much responsibility here. The problem of contract discovery is difficult to solve because different applications may have different criteria for which contracts they want to follow, and many of those criteria require domain knowledge that the runtime is not aware of (even the simple case of following all contracts related to a wallet is difficult because we need to have wallet awareness to do this). Therefore, I propose that we solve this problem by yet again deferring some of the complexity. Instead of a general-purpose We could provide utilities that make this job easier. For example, we could define the notion of a "contract header" that can be used to identify and classify the contract, and provide an API that could page through all contract headers, possibly providing filtering at that point. |
Beta Was this translation helpful? Give feedback.
-
Marlowe History ERD V1: erDiagram
contract ||--o{ inputApplication : "advanced by"
contract ||--o{ party : "participants of"
inputApplication ||--o{ input : applies
input ||--o{ depositAsset : deposits
inputApplication ||--o{ payment : produces
payment ||--o{ paymentAsset : transfers
paymentAsset ||--o{ asset : "classified by"
depositAsset ||--o{ asset : "classified by"
party ||--o{ payment : receives
party ||--o{ payment : sends
payment ||--o| paymentRedemption : "redeemed by"
contract {
txOutRef contractId PK "NOT NULL"
bigint slotNo "NOT NULL"
bytea policyId "NOT NULL"
jsonb contract "NOT NULL"
bytea applicationValidatorHash "NOT NULL"
bytea payoutValidatorHash "NOT NULL"
}
inputApplication {
txOutRef inputApplicationId PK "NOT NULL"
txOutRef contractId FK "NOT NULL"
bigint slotNo "NOT NULL"
txOutRef txOut
}
party {
serial partyId PK "NOT NULL"
txOutRef contractId FK "NOT NULL"
bytea address "NOT NULL if role IS NULL and NULL if role IS NOT NULL"
bytea role "NOT NULL if address IS NULL and NULL if address IS NOT NULL"
}
payment {
serial paymentId PK "NOT NULL"
int fromParty FK "NOT NULL"
int toParty FK "NOT NULL"
txOutRef inputApplicationId FK "NOT NULL"
bigint lovelace "NOT NULL if txOut is NOT NULL"
txOutRef txOut
}
asset {
serial assetId PK "NOT NULL"
bytea policyId "NOT NULL"
bytea name "NOT NULL"
}
paymentAsset {
int assetId FK "NOT NULL"
int paymentId FK "NOT NULL"
bigint quantity "NOT NULL"
}
paymentRedemption {
bytea txOut PK "NOT NULL"
bigint slotNo "NOT NULL"
bytea txId "NOT NULL"
}
input {
smallint inputNo PK "NOT NULL"
txOutRef inputApplicationId FK "NOT NULL"
jsonb continuationContract
inputType tag "NOT NULL"
int depositToAccount FK "NOT NULL if tag = deposit"
int depositFromParty FK "NOT NULL if tag = deposit"
bigint depositLovelace "NOT NULL if tag = deposit"
bytea choiceName "NOT NULL if tag = choice"
int choiceParty FK "NOT NULL if tag = choice"
bigint choiceValue "NOT NULL if tag = choice"
}
depositAsset {
int assetId FK "NOT NULL"
smallint inputNo FK "NOT NULL"
txOutRef inputApplicationId FK "NOT NULL"
bigint quantity "NOT NULL"
}
|
Beta Was this translation helpful? Give feedback.
-
I vetted my initial state diagram of the Marlowe Sync Protocol by writing out the types and implementing the client and server peers. Here is a revised version based on changes that I made where the initial idea didn't stand up to scrutiny: stateDiagram-v2
[*] --> Init
Init --> Follow : FollowContract
Follow --> Idle : ContractFound
Follow --> Done : ContractNotFound
Idle --> Next : RequestNext
Idle --> Done : Done
Next --> Idle : InputsApplied
Next --> Idle : Closed
Next --> Idle : PaymentRedeemed
Next --> Idle : RollBackward
Done --> [*]
state Idle {
direction LR
[*] --> Pending : RollBackward
[*] --> Running : RollBackward
[*] --> Running : ContractFound
[*] --> Running : InputsApplied
[*] --> Running : PaymentRedeemed
[*] --> Closed : RollBackward
[*] --> Closed : ContractFound
[*] --> Closed : Closed
[*] --> Closed : PaymentRedeemed
Running --> [*] : Done
Running --> [*] : RequestNext
Closed --> [*] : Done
Closed --> [*] : RequestNext
Pending --> [*] : Done
}
state Next {
direction LR
[*] --> nextRunning : RequestNext
[*] --> nextClosed : RequestNext
nextRunning : Running
nextClosed : Closed
state nextRunning {
direction LR
[*] --> RunningCanWait : RequestNext
RunningCanWait : CanWait
RunningMustReply : MustReply
RunningCanWait --> RunningMustReply : Wait
RunningCanWait --> [*] : InputsApplied
RunningCanWait --> [*] : PaymentRedeemed
RunningCanWait --> [*] : Closed
RunningCanWait --> [*] : RollBackward
RunningMustReply --> [*] : InputsApplied
RunningMustReply --> [*] : PaymentRedeemed
RunningMustReply --> [*] : Closed
RunningMustReply --> [*] : RollBackward
}
state nextClosed {
direction LR
[*] --> ClosedCanWait : RequestNext
ClosedCanWait : CanWait
ClosedMustReply : MustReply
ClosedCanWait --> ClosedMustReply : Wait
ClosedCanWait --> [*] : PaymentRedeemed
ClosedCanWait --> [*] : RollBackward
ClosedMustReply --> [*] : PaymentRedeemed
ClosedMustReply --> [*] : RollBackward
}
nextRunning --> [*] : InputsApplied
nextRunning --> [*] : PaymentRedeemed
nextRunning --> [*] : Closed
nextRunning --> [*] : RollBackward
nextClosed --> [*] : PaymentRedeemed
nextClosed --> [*] : RollBackward
}
|
Beta Was this translation helpful? Give feedback.
-
For completeness, here are a couple more general protocols which we will need here and in other components: Query Protocol: stateDiagram-v2
[*] --> Init
Init --> Next : Request(req delimiter err result)
Next --> Done : Reject(err)
Next --> Page : NextPage(result, Maybe delimiter)
Page --> Next : RequestNext(delimiter)
Page --> Done : Done
Done --> [*]
state Next {
[*] --> CanReject : Request(req delimiter err result)
[*] --> MustReply : RequestNext(delimiter)
CanReject --> [*] : Reject(err)
CanReject --> [*] : NextPage(result, Maybe delimiter)
MustReply --> [*] : NextPage(result, Maybe delimiter)
}
Notes:
Async Command Protocol: stateDiagram-v2
[*] --> Init
Init --> Cmd : Command(cmd)
Init --> Cmd : Resume(id)
Cmd --> Await : Await(id, progress)
Await --> Poll : Poll
Poll --> Await : Update(progress)
Cmd --> Done : Fail(err)
Cmd --> Done : Succeed(result)
Poll --> Done : Fail(err)
Poll --> Done : Succeed(result)
Done --> [*]
Notes:
Expectations:
Implications:
|
Beta Was this translation helpful? Give feedback.
-
I just revisited the Marlowe Sync Protocol and decided to make several simplifications. Instead of representing the stage of a contract's lifecycle in sub-states, there is only one stateDiagram-v2
[*] --> Init
Init --> Follow : FollowContract
Init --> Intersect : Intersect
Follow --> Idle : ContractFound
Follow --> Done : ContractNotFound
Idle --> Next : RequestNext
Intersect --> Idle : IntersectFound
Intersect --> Done : IntersectNotFound
Idle --> Done : Done
Next --> Idle : RollForward
Next --> Idle : RollBackward
Next --> Wait : Wait
Wait --> Next : Poll
Wait --> Idle : Cancel
Done --> [*]
|
Beta Was this translation helpful? Give feedback.
-
This thread is for exploratory brainstorming and fleshing out the requirements of the runtime history module. Here is what we have to work with at the moment:
Here is a possible architecture that could meet these needs:
History is (or is part of) a long-running background process. It maintains several threads of control:
ContractFilter
component connects to the Chain Sync via the Filtered Chain Sync Protocol. It receives aContractFilter
as an input. Its job is to traverse the chain to search for contracts that match the filter (for example, it could watch for role token deposits in a user's wallet). When it discovers a new contract, it spawns aContractFollower
component. It exposes anSTM ContractEvents
action, aggregating the events from the activeContractFollower
s.ContractFollower
component connects to the Chain Sync via the Filtered Chain Sync Protocol. It receives aContractId
(TxOut reference that created the contract) as an input. Its job is to follow the events of the contract on chain. It exposes anSTM ContractEvents
action that retrieves unsaved events (similar to theSTM Changes
exposed by theNodeClient
in the Chain Sync). It dies when the contract is closed after allowing the security parameter to elapse (so it can be sure it won't get rolled back).ContractStore
component reads from theSTM ContractEvents
action exposed by theContractFilter
in a loop, writing unsaved contract events to a database. It exposes anSTM (Map ContractId UTCTime)
action, which it updates every time it saves new contract events. It also exposes aHistoryQuery a -> m a
action for running queries.QueryServer
component awaits queries and responds to them by running theHistoryQuery a -> m a
action exposed by theContractStore
.SyncServer
component implements the Server role in the Marlowe History Sync Protocol (see below). Clients connect to this server to synchronize contract history.The Marlowe History Sync Protocol would function similarly to the Chain Sync Protocol. I won't get into the detailed design here, but in short, a client would initialize the connection by providing a contract filter to control which contracts the client sees. From the idle state, the client could choose to enter one of four modes:
Beta Was this translation helpful? Give feedback.
All reactions