From a5ee6b7c0b951c4aa55e2f51b27302a67b202088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 9 Jul 2024 17:39:55 +0300 Subject: [PATCH 01/13] docs: cip draft --- README.md | 20 +- docs/01-Stream-api.md | 137 -------- docs/02-Synchronization-Sequence.svg | 1 - docs/02-Synchronization.md | 3 - docs/03-Encoding.md | 20 -- docs/CIP/CIP-XXXX/README.md | 259 ++++++++++++++++ .../images/02-Synchronization-Sequence.svg | 1 + .../CIP-XXXX}/messages/client/heartbeat.md | 0 .../CIP/CIP-XXXX/messages/client/subscribe.md | 293 ++++++++++++++++++ .../CIP-XXXX/messages/server}/era-summary.md | 0 .../CIP-XXXX}/messages/server/genesis.md | 0 .../messages/server}/protocol-parameters.md | 0 docs/CIP/CIP-XXXX/messages/server/rewards.md | 118 +++++++ .../{ => CIP/CIP-XXXX}/messages/server/tip.md | 0 .../CIP-XXXX}/messages/server/transaction.md | 0 docs/CIP/CIP-XXXX/messages/server/welcome.md | 125 ++++++++ .../src/02-Synchronization-Sequence.puml} | 22 +- docs/CPS/CPS-XXXX/README.md | 132 ++++++++ .../Lace_Fundamental_State_Sequence_Flow.png | Bin 0 -> 96319 bytes .../Lace_Fundamental_State_Sequence_Flow.puml | 57 ++++ docs/messages/client/subscribe.md | 79 ++++- docs/messages/client/unsubscribe.md | 36 --- docs/messages/index.md | 157 ---------- 23 files changed, 1065 insertions(+), 395 deletions(-) delete mode 100644 docs/01-Stream-api.md delete mode 100644 docs/02-Synchronization-Sequence.svg delete mode 100644 docs/02-Synchronization.md delete mode 100644 docs/03-Encoding.md create mode 100644 docs/CIP/CIP-XXXX/README.md create mode 100644 docs/CIP/CIP-XXXX/images/02-Synchronization-Sequence.svg rename docs/{ => CIP/CIP-XXXX}/messages/client/heartbeat.md (100%) create mode 100644 docs/CIP/CIP-XXXX/messages/client/subscribe.md rename docs/{messages/server/cardano => CIP/CIP-XXXX/messages/server}/era-summary.md (100%) rename docs/{ => CIP/CIP-XXXX}/messages/server/genesis.md (100%) rename docs/{messages/server/cardano => CIP/CIP-XXXX/messages/server}/protocol-parameters.md (100%) create mode 100644 docs/CIP/CIP-XXXX/messages/server/rewards.md rename docs/{ => CIP/CIP-XXXX}/messages/server/tip.md (100%) rename docs/{ => CIP/CIP-XXXX}/messages/server/transaction.md (100%) create mode 100644 docs/CIP/CIP-XXXX/messages/server/welcome.md rename docs/{02-Synchronization.puml => CIP/CIP-XXXX/src/02-Synchronization-Sequence.puml} (82%) create mode 100644 docs/CPS/CPS-XXXX/README.md create mode 100644 docs/CPS/CPS-XXXX/images/Lace_Fundamental_State_Sequence_Flow.png create mode 100644 docs/CPS/CPS-XXXX/src/Lace_Fundamental_State_Sequence_Flow.puml delete mode 100644 docs/messages/client/unsubscribe.md delete mode 100644 docs/messages/index.md diff --git a/README.md b/README.md index 28bd66f..2f80fd7 100644 --- a/README.md +++ b/README.md @@ -2,25 +2,13 @@ The primary goal of a well-designed API for a multi-chain digital wallet is to provide the data required to construct transactions and to allow deriving the current state of the wallet while the set of subscribed blockchains is continuously extended. This includes aggregating transactions and on-chain events to present users their transaction history, current balance, as well as chain-specific features such as stake delegation or staking rewards. -The proposed API design offers different endpoints to retrieve the same data in order to support a wide range of edge clients, +The proposed API design offers different endpoints to retrieve the same data in order to support a wide range of edge clients, in particular clients with an intermittent connection or that are bandwidth-constrained. The API was desgined with an overarching abstraction in mind to focus on the fundamental value that wallets provide: -> #### The ability to transact +> The ability to transact -### What does a wallet need to construct transactions? +## Problem Description [CPS](./docs/CPS/CPS-XXXX/) -In general, to be able to construct transactions the following data is required: - -- any transaction related to a client's wallet (incoming/ receiving & outgoing/ spending) -- staking rewards *if applicable* -- network, era or epoch specific data, like: - - protocol parameters - tx fee calculation (Cardano) - - current fee rate, satoshis per byte, sats/byte (Bitcoin) - - gas limit, max fee per gas, max priority fee per gas, nonce (Ethereum) -- the current tip/ block height for validity intervals of transactions - -## API Design - -We divide the wallet-optimized API into a [push-based, event-driven API](./docs/01-Stream-api.md) and a request/ response API. \ No newline at end of file +## Improvement Proposal [CIP](./docs/CIP/CIP-XXXX/) diff --git a/docs/01-Stream-api.md b/docs/01-Stream-api.md deleted file mode 100644 index 11ad856..0000000 --- a/docs/01-Stream-api.md +++ /dev/null @@ -1,137 +0,0 @@ -# Push-based API Overview - -The push-based API for digital wallets is designed to provide clients with a continuous stream of **relevant** on-chain events, enabling them to derive their current wallet state efficiently. This API supports various blockchain models, including UTXO-based chains and account-based chains. Additionally, clients receive secondary data relevant to the blockchain network they are subscribed to, ensuring comprehensive and up-to-date information. - -The following serves as introduction to the wallet-optimized, event-driven API using **websockets**. -This document outlines the structure and common elements of messages that clients may process as well as the overall protocol. - -## Protocol - -We use a bidirectional protocol and encode all messages as JSON objects. Messages adhere to a specific structure, ensuring consistency and facilitating efficient communication within our event-driven protocol. As we strive to support multiple blockchains, a server-sent message will always reference the originating chain it relates to. - -### Message Format - -Instead of relying on typed messages that may vary highly depending on the underlying blockchain, our design aims to enrich and aggregate data in compound messages. - -#### Aggregate Compound Message Format - -Any server-sent message consists of multiple top level keys with client-relevant data. Below, one can see how a chain reference is part of -a generic server-sent message: - -```json -{ - /* top level message fields like transaction, genesis, rewards, point etc. */ - "chain": { - "blockchain": "cardano", - "network": "mainnet" - /* other blockchain specific fields */ - } -} -``` - -Furthermore, any server-sent message is accompanied by a specific timestamp indicating the exact point in time within the respective chain. - -### Tracking Time on Blockchains - -Blocks on blockchains are often compared to the heartbeat of the chain, representing their own measurement of time. However, the actual time it takes to produce a block on blockchains is non-deterministic. Many independent actors within the distributed system compete to build the next block via Proof-of-Work (PoW), (delegated) Proof of Stake (dPoS/PoS) or another consensus mechanism. Consequently, blocks are produced on a probabilistic basis but are eventually evenly distributed over time with the help of algorithms like Bitcoin's difficulty adjustment or Cardano's protocol parameters that define how frequently blocks can occur for a predefined time period, termed _epoch_. - -Our API needs a reliable measurement of time to construct a stream of ordered, client-relevant, on-chain events that connected clients can easily consume. Time is crucial for clients for various reasons, including: - -- monitoring the progress of synchronization up to the chain's tip -- defining transaction validity intervals - -> [!NOTE] -> Transaction validity intervals allow clients to submit transactions with a specified lifetime, which serves several important purposes: -> -> - protects against replay attacks by ensuring transactions can't be resubmitted after expiration -> - helps manage network congestion by allowing expired transactions to be discarded -> - supports operations that need to occur within specific timeframes - -Some blockchains, like Cardano, define a notion of time based on slots, which are fixed intervals of time (typically seconds). Slot numbers either represent elapsed seconds since the network's genesis block (absolute) or beginning of epoch (epoch). In contrast, other blockchains like Bitcoin don't use a fixed time interval and instead rely on an intrinsic clock derived from block production for transaction validity intervals. - -### Our Protocol's Way of Tracking Time - -We borrow the term `point` from the Cardano blockchain to create an abstract notion of time that is sent along side any sever-sent message. By default, whenever a new transaction is deemed relevant for a connected client, the server enriches the message by adding the current "`point` in on-chain time" of the respective chain. That may look different depending on the blockchain a message relates to: - -#### Bitcoin Point - -```json -{ - "point": { - "height": 849561, - "hash": "00000000000000000000e51f4863683fdcf1ab41eb1eb0d0ab53ee1e69df11bb" - } - /* ... */ -} -``` - -#### Cardano Point - -```json -{ - "point": { - "slot": 127838345, - "hash": "9f06ab6ecce25041b3a55db6c1abe225a65d46f7ff9237a8287a78925b86d10e" - } - /* ... */ -} -``` - -#### Ethereum Point - -```json -{ - "point": { - "height": 20175853, - "hash": "0xc5ae7e8e0107fe45f7f31ddb2c0456ec92547ce288eb606ddda4aec738e3c8ec" - } - /* ... */ -} -``` - -> [!NOTE] -> -> New top level message keys can be added at any time. -> Clients are expected to iterate over messages and skip those they do not support. - -More information for time and event sequencing can be found in the section [Event Sequencing and Synchronization](./messages/index.md#event-sequencing-and-synchronization). - -### Error Handling - -Any cases of failure trigger an error message that is appended to a list of `errors`: - -```json -{ - "errors": [ - { - "type": "error type", - "message": "error message" - } - ] - /* ... */ -} -``` - -## Message Types - -We distinguish between _client-sent_ and _server-sent_ messages. Client messages are typed messages. Each message defines a `type`, whereby server-sent messages follow the aggregated, compound message structure explained above. An index of client message types and server-sent message parts is detailed [here](./messages/index.md). - -## Keep Alive/ Heartbeat Messages - -To conserve server resources, we shift connection maintenance responsibility to clients. This allows the server to terminate any inactive connections not regularly refreshed by clients. - -More information can be found [here](./messages/client/heartbeat.md). - -## Authentication - -API authentication in its first version is implicitly handled by the client specifying the topics of interest in their initial [`subscribe`](./messages/client/subscribe.md) message. This message must include a **signature** that verifies the ownership of the provided credentials, which are used to filter block transactions. - -More information can be found [here](./messages/client/subscribe.md). - -## Other Topics - -[1. Message Type Index](./messages/index.md) - -[2. Synchronization](./02-Synchronization.md) - -[3. Encoding Standard](./03-Encoding.md) diff --git a/docs/02-Synchronization-Sequence.svg b/docs/02-Synchronization-Sequence.svg deleted file mode 100644 index f054d94..0000000 --- a/docs/02-Synchronization-Sequence.svg +++ /dev/null @@ -1 +0,0 @@ -WalletWalletServerServerDBDBProjectorProjectorCardano NodeCardano NodeTask QueueTask QueueProjection WorkerProjection WorkerWallet Connectsconnect(credentials, points: PointOrOrigin[])findIntersection(points)response: intersection point or genesisalt[if point is genesis]<event> {network genesis parameters}Catch-up Phase: Get intersection eventsprotocol parametersprotocolParametersSince(point)response: protocolParameters: [protocol-parameters]era summarieseraSummariesSince(point)response: eraSummaries: [era-summaries]epoch summariesstake summariesstakeSummariesSince(point)response: stakeSummaries: [stake-summary]supply summariessupplySummariesSince(point)response: suppleSummaries: [supply-summary]rewardsrewardsSince(point, credentials)response: rewardsSincePoint: [rewards]transactionstransactionsSince(point, credentials)response: transactionsSincePoint: [transaction + block header]alt[if transaction contains native assets]assetInfo(assetId[])response: assetInfo: [asset meta data]Enrich transaction messageswith native asset meta data.Merge, sort responses by point.<event>{[transactions + block header],[rewards],[epoch summary],[era summary],[protocol parameters]}Wallet Synchronization<event> Chain Syncwrite & notify(extendedBlock)<event> notify(extendedBlock)loop[For each connected client]alt[if block tx is relevant for client:]Enrich tx message(tip, pp etc.)<event> { [transaction + block header] }[if minimum tip threshold interval passed:]<event> { tip }Asynchronous projection<event> Chain Syncinsert or delete taskgetTask()return taskinsert & notify<event> {metadata: {pool:{id, metadata}}<event> {metadata: {pool:{id, metadata}} \ No newline at end of file diff --git a/docs/02-Synchronization.md b/docs/02-Synchronization.md deleted file mode 100644 index 580b450..0000000 --- a/docs/02-Synchronization.md +++ /dev/null @@ -1,3 +0,0 @@ -# Synchronization - -![Sequence Diagram Synchronization](./02-Synchronization-Sequence.svg) \ No newline at end of file diff --git a/docs/03-Encoding.md b/docs/03-Encoding.md deleted file mode 100644 index b219a56..0000000 --- a/docs/03-Encoding.md +++ /dev/null @@ -1,20 +0,0 @@ -# Encoding Standards - -Our proposed communication protocol **adopts blockchain-native encoding standards** to ensure compatibility and efficiency. This approach leverages existing conventions familiar to clients while minimizing conversion overhead. The protocol incorporates the following encoding standards: - -## Transaction Hash Encoding - -Transaction hashes are typically encoded using **hexadecimal representation**. This standard provides a compact and human-readable format for unique transaction identifiers. - -## Address Encoding - -Address encoding varies depending on the blockchain network: - - Bitcoin: Uses Base58Check encoding for legacy addresses and `Bech32` for SegWit addresses. - - Ethereum: Employs hexadecimal encoding with a `0x` prefix. - - Cardano (and others): use custom address formats (e.g., `Bech32` variants). - - -## Native Asset Policy Encoding - -For blockchain networks supporting native assets (e.g., Cardano), policy identifiers are typically encoded in hexadecimal format. - diff --git a/docs/CIP/CIP-XXXX/README.md b/docs/CIP/CIP-XXXX/README.md new file mode 100644 index 0000000..07d2f0f --- /dev/null +++ b/docs/CIP/CIP-XXXX/README.md @@ -0,0 +1,259 @@ +--- +CIP: 1 +Title: Event-driven, wallet-optimized API +Status: Proposed +Category: Wallets +Authors: + - William Wolff + - Hızır Sefa İrken + - Rhys Bartels-Waller + - Martynas Kazlauskas + - Daniele Ricci +Implementors: N/A +Solution-To: CPS-???? +Discussions: +Created: 2024-07-10 +License: CC-BY-4.0 +--- + +## Abstract + +We propose a multi-chain, event-driven, push-based API optimized for wallet applications, leveraging the principle that a wallet's state should be a pure function of the blockchain state. This approach eliminates polling strategies and aggregating views, enabling real-time synchronization with the blockchain as new blocks are appended. Our solution addresses key challenges in current wallet implementations, including rollback handling, address discovery and session management for serving stateful clients. + +## Motivation: why is this CIP necessary? + +The CIP provides a partial solution to the problems described in [CPS-????](../CPS/CPS-XXXX/README.md). In particular, we strive to define a standardized approach serving data to edge clients in an unopinionated, close to blockchain native format using a push-based protocol. + +## Specification + +### Introduction + +#### What does a wallet need to construct transactions? + +- past transactions (receiving & spending) to derive balance, UTxO set, delegation and governance state +- reward account balance (staking, voting) +- network, era or epoch specific data, like: + - protocol parameters (tx fees, plutus cost models, ...) + - genesis (security param, slot length) +- the current tip/ block height for validity intervals of transactions as well as sync progress + +#### How do we serve this data? + +Through a push-based API, wallets are provided with a continuous stream of **relevant** on-chain events, enabling them to derive their current wallet state efficiently. This API supports various blockchain models, including UTXO-based chains and account-based chains. Additionally, clients receive secondary data relevant to the blockchain network they are subscribed to, ensuring comprehensive and up-to-date information. + +#### Protocol + +We use a bidirectional protocol and encode all messages as JSON objects, while transactions contained in messages are preserved in the chain's native encoding standard (cbor). Messages adhere to a specific structure, ensuring consistency and facilitating efficient communication within our event-driven protocol. As we strive to support multiple blockchains, a server-sent message will always reference the originating chain it relates to. + +### Message Types & Structure + +We distinguish between client-sent and server-sent messages. + +#### Client + +Client-sent messages are **typed** messages, such as: + +```json +{ + "type": "" + // ... +} +``` + +This allows the server to quickly identify the client's intent. More details on what typed messages exist will be covered in [Client Typed Messages](#client-typed-messages) section. + +#### Server + +Instead of typed messages, server-sent messages vary in structure depending on the underlying blockchain. Our design aims to be chain-agnostic and serve data from different blockchains in future. Therefore, we rely on **untyped** messages that are enriched and aggregated into compound messages. We call each top level key a server message partial. + +This leads to a structure that has many top level keys of which, not every key may be present for every message, for example: + +```json +{ + "transactions": [ + /* ... */ + ], + "resolvedInputs": [ + /* ... */ + ], + "rewards": { + /* ... */ + }, + "tip": { + /* ... */ + } + // ... +} +``` + +> [!IMPORTANT] +> +> New message partials can be added at any time, which allows for flexible extension of the API. +> Clients are expected to check for message key presence and ignore those they do not support. + +#### Errors + +Any cases of server-side failure trigger an error message that is append as separate `error` message partial: + +```json +{ + "error": { + "type": "error type", + "message": "error message" + } + /* ... */ +} +``` + +### Authentication + +A newly connected client authenticates as part of the [`subscribe`](./messages/client/subscribe.md) message that is sent to the server to synchronize a specific wallet account up to the chain's current tip. This message must include a **signature** that verifies the ownership of provided extended public key (xpub key). + +Extended public keys (xpub keys) serve a dual purpose in our design: **authentication** and **efficient transaction querying**. Here's how it works: + +#### Account Authentication + +Clients connect and authenticate via subscribe messages by providing: + +- a digital signature +- a public key whose Blake2b-256 hash corresponds to one of their provided credentials: payment and stake key hash(es) or script hash (es) in case of shared wallets + +This ensures that only authorized clients can access their account data. + +The `signature` field is generated by signing with the private payment/ stake key a SHA256 HMAC hashed string. The prehash string is constructed by concatenating the subscribed blockchain + timestamp. + +The server verifies the signature, timestamp and whether the Blake2b-256 hash of public key matches one of the credentials before serving any data to the client. If any part of the `subscribe` message is invalid, the server sends an error and closes the connection. + +#### Transaction Querying + +The credentials also enable efficient querying of client-relevant data: + +1. **Indexing Strategy** + + We index transactions by both payment and stake credential. This dual-indexing approach allows for comprehensive transaction tracking. + +2. **BIP32 Wallet Query Process** + + The server queries our index using the corresponding key hashes per client to serve only relevant transactions to scubribed clients. + +### Ordering of Events + +Our API needs a reliable measurement of time to construct a stream of ordered, client-relevant, on-chain events that connected clients can easily consume. Time is crucial for clients for various reasons, including: + +- monitoring the progress of synchronization up to the chain's tip +- defining transaction validity intervals + +> +> - supports operations that need to occur within specific timeframes +> - helps manage network congestion by allowing expired transactions to be discarded +> - protects against replay attacks by ensuring transactions can't be resubmitted after expiration + +We borrow the term `point` from the Cardano blockchain to introduce a server-sent message partial. For instance, if a new transaction is deemed relevant for a connected client, the server enriches the message by adding the current "`point` in on-chain time" of the respective chain. For Cardano, this typically looks as follows: + +#### Cardano Point + +```json +{ + "point": { + "slot": 127838345, + "hash": "9f06ab6ecce25041b3a55db6c1abe225a65d46f7ff9237a8287a78925b86d10e" + } + /* ... */ +} +``` + +> [!NOTE] +> Clients may safely assume that any server-sent message, aside from error messages contain `point` partial. + +### Synchronization + +We refer to synchronization as the process that a wallet undertakes to catch up to the current chain's tip in order to derive its latest state and proceed to transact. The aforedescribed `point` represents the main anchor for wallets, to be able to: + +- resume the sync process since their last known point +- keep track of their current position in the blockchain +- track and handle chain reorganizations (rollbacks) + +The following sequence diagram shows, how a client connects providing an array of `point`s and extended public key. The client provides more than those two bits which is described in the [`subscribe`](./messages/client/subscribe.md) message. +Subsequently, the server tries to find an intersection given the list of `point`s. + +For an identified intersection, the server computes and publishes an ordered stream of events. +This event stream provides a chronological record of on-chain events and affectively state changes for the wallet to derive its latest state. + +> [!NOTE] +> The server sources client-relevant data through credential-dependent, point-specific database queries, though the implementation details are beyond this CIP's scope. + +![Sequence Diagram Synchronization](./images/02-Synchronization-Sequence.svg) + +#### Resumability + +One of the traditional operational challenges of serving stateful clients is session management during times of scaling. In our protocol, the server can disconnect clients at any time while clients can resume synchronization from their last known `point`s after reconnecting. To minimize reconnection attempts, clients should provide a list of `point`s based on their most recent state, including some that may have been affected by rollbacks. This approach reduces the likelihood of the server failing to find a valid intersection, which would otherwise result in dropped connections and additional round trips. + +> [!NOTE] +> More details are described in the [`subscribe`](./messages/client/subscribe.md) message. + +#### Rollback Handling + +In a [CAP](https://en.wikipedia.org/wiki/CAP_theorem) system like Cardano, which balances global consistency with availability, there is a challenge in dealing with rollback events that impact transaction finality. Regardless of whether one uses their own full node or a server provider for transaction submission - if such events are not properly handled with the help of monitoring the rolling window of [k blocks](https://plutus-apps.readthedocs.io/en/latest/plutus/explanations/rollback.html) and respective resubmission, there is no guarantee that the transaction is finalized, eg. becomes part of the immutable pefix of the Cardano blockchain. + +Since our wallet-optimized protocol defines that server-sent messages include the `point` message partial, the client can check if its last `point` is greater than any subsequent message `point` from the server, without the need to introduce a `rollback` specific message partial. This assumes that the client keeps a local copy of the volatile segment of the blockchain defined by the security parameter. + +#### Connection Management + +Clients are expected to reconnect on error, when providing invalid `point`s for which a server cannot find an intersection. +Clients may send multiple [`subscribe`](./messages/client/subscribe.md) messages for different accounts, but subsequently take responsibility to coordinate server-sent events by time to their possibly differently synchronized accounts. When receiving a message that references a `point` that the first account has already seen, the client only applies it for its second account for example. + +### Versioning + +We propose to version message partials individually. In our protocol, the client sends its list of supported versions as part of the initial [`subscribe`](./messages/client/subscribe.md) message. Upon receiving this information, the server selects the highest mutually supported version and associates it with the client. +This method requires the server to implement version-based message construction, allowing it to tailor its responses to each client's supported version. + +### Encoding + +Our proposed communication protocol **adopts blockchain-native encoding standards** to ensure compatibility and efficiency. This approach leverages existing conventions familiar to clients while minimizing conversion overhead on the server-side. (see [CPS - Fragile API](../../CPS/CPS-XXXX/README.md#fragile-api)) + +#### Address Encoding + +Address encoding varies depending on the blockchain network. In Cardano the protocol servers bech32 encoded addresses. + +### Extensions + +Clients can turn on certain `extensions` by setting a respective flags in their subscription [`config](./messages/client/subscribe.md#subscribe-example) which tells the server to for example: + +- to enrich transaction output asset values by their respective off-chain metadata such as decimal offsets +- to resolve transaction inputs (transaction output references) as transaction outputs + +## Rationale: how does this CIP achieve its goals? + +A primary consideration for the protocol design was to minimize message round trips between client and server as well as the amount of work to be done by the server for each client. Thus, the API's authentication does not require an initial message exchange but rather defines a standard verifable method for the server to either drop a client early and proceed to publish its relevant data. + +### Client Typed Messages + +- [`heartbeat`](./messages/client/heartbeat.md): Connection management/ keep alive + +> [!NOTE] +> Since the API supports extensions, transport-specific messages can be implemented such as this. + +- [`subscribe`](./messages/client/subscribe.md): Define topics of interest & authentication + +### Server Message Partials + +- [`welcome`](./messages/server/welcome.md): Service details, sent once immediately upon establishing a connection +- [`genesis`](./messages/server/genesis.md): If applicable for the subscribed blockchain, it serves static data used as part of the network bootstrap +- [`transaction`](./messages/server/transaction.md): A new transaction +- [`tip`](./messages/server/tip.md): A new block was appended +- [`protocol parameters`](./messages/server/protocol-parameters.md): Epoch based message for updated protocol parameters +- [`era summary`](./messages/server/era-summary.md): Era based message for updated slot length + +## Path to Active + +### Acceptance Criteria + +[!TODO] + +### Implementation Plan + +[!TODO] + +## Copyright + +This CIP is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode). diff --git a/docs/CIP/CIP-XXXX/images/02-Synchronization-Sequence.svg b/docs/CIP/CIP-XXXX/images/02-Synchronization-Sequence.svg new file mode 100644 index 0000000..9dde2f3 --- /dev/null +++ b/docs/CIP/CIP-XXXX/images/02-Synchronization-Sequence.svg @@ -0,0 +1 @@ +WalletWalletServerServerDBDBProjectorProjectorCardano NodeCardano NodeTask QueueTask QueueProjection WorkerProjection WorkerWallet Connects<event> welcome {service details}subscribe(credentials, points: PointOrOrigin[])findIntersection(points)response: intersection point or genesisalt[if no intersection found]<event> { invalid point error }subscribe(points: PointOrOrigin[])alt[if point is genesis]<event> {network genesis parameters}Replay Phase: Get intersection eventsprotocol parametersprotocolParametersSince(point)response: protocolParameters: [protocol-parameters]era summarieseraSummariesSince(point)response: eraSummaries: [era-summaries]epoch summariesstake summariesstakeSummariesSince(point)response: stakeSummaries: [stake-summary]supply summariessupplySummariesSince(point)response: suppleSummaries: [supply-summary]rewardsrewardsSince(point, credentials)response: rewardsSincePoint: [rewards]transactionstransactionsSince(point, credentials)response: transactionsSincePoint: [transaction + block header]alt[if transaction contains native assets]assetInfo(assetId[])response: assetInfo: [asset meta data]Enrich transaction messageswith native asset meta data.Merge, sort responses by point.<event>{[transactions + block header],[rewards],[epoch summary],[era summary],[protocol parameters]}Follow Tip<event> Chain Syncwrite & notify(extendedBlock)<event> notify(extendedBlock)loop[For each connected client]alt[if block tx is relevant for client:]Enrich tx message(tip, pp etc.)<event> { [transaction + block header] }[if minimum tip threshold interval passed:]<event> { tip } \ No newline at end of file diff --git a/docs/messages/client/heartbeat.md b/docs/CIP/CIP-XXXX/messages/client/heartbeat.md similarity index 100% rename from docs/messages/client/heartbeat.md rename to docs/CIP/CIP-XXXX/messages/client/heartbeat.md diff --git a/docs/CIP/CIP-XXXX/messages/client/subscribe.md b/docs/CIP/CIP-XXXX/messages/client/subscribe.md new file mode 100644 index 0000000..dc85924 --- /dev/null +++ b/docs/CIP/CIP-XXXX/messages/client/subscribe.md @@ -0,0 +1,293 @@ +# Client Subscribe Message + +Sent by client to server right after establishing a connection. New clients must authenticate themselves when subscribing to any blockchain events via a signature. + +## Schema + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["subscribe"] + }, + "topic": { + "type": "array", + "items": { + "type": "object", + "properties": { + "blockchain": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "network": { + "type": "string" + } + }, + "required": ["name", "network"] + }, + "signature": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + }, + /* */ + "cardano": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "properties": { + "payment": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + } + }, + "stake": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + } + } + }, + "required": ["payment", "stake"] + }, + "points": { + "type": "array", + "properties": { + "slot": { + "type": "integer", + "minimum": 0 + }, + "hash": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + } + }, + "required": ["slot", "hash"] + }, + "extensions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": {}, + "version": { + "type": "string" + } + }, + "required": ["name", "config", "version"] + } + } + }, + "required": ["credentials", "extensions", "points"] + } + }, + "required": ["blockchain", "signature"] + } + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + }, + "required": ["type", "topic", "timestamp"], + "additionalProperties": false +} +``` + +The `signature` field is generated by signing a SHA256 HMAC hashed string. The prehash string is constructed by concatenating the subscribed `blockchain` + `timestamp`. + +> [!NOTE] +> For subscribing to multiple blockchains, the prehash string must be generated for each as `.`, for example: +> `cardano.mainnet+2024-06-27T12:34:56Z`. + +Apply a SHA256 HMAC using either the payment signing key or staking signing key, and then base64-encode it as final payload within the initial message. Pass the output of this SHA256 HMAC to the signature field. + +The signature verifies ownership of the private key, whose corresponding public key is used to filter relevant block transactions. + +## Subscribe Example + +```json +{ + "type": "subscribe", + "topic": { + "blockchain": { "name": "cardano", "network": "mainnet" }, + "version": ["0.1"], + "publicKey": "abc", + "signature": "OGNiOWIyNGVjOTMxZmY3N2MzYjQxOTY3OWE0YTcwMzczZmVkZmIxNDZmMDE0ODk0Nzg4YjUxMmIzMjE4MDdiYw==", // base64, SHA256 HMAC with your signing key + "cardano": { + "credentials": { + "payment": [ "0d166978f407505f157c9f56fdb3358e60d1589295b2f7a1e66c1574" ], + "stake": [ "82c00414a674fd7e7657aa5634e2086910c2f210e87f22ce880a0063" ] + }, + "points": [ + { + "slot": 66268628, + "hash": "47b8ec3a58a4a69cb5e3397c36cb3966913882fa8179cae10a5d3f9319c4ae66" + }, + { + "slot": 87868775, + "hash": "074985b22edc01b9579a2e571dc125e044aecf812ee45d50e6fb6fef979fd0d0" + } + ], + "extensions": [ + { + "name": "resolveTxInput", + "config": true, + "version": "1.0" + }, + { + "name": "assetMetadata", + "config": false, + "version": "1.0" + } + ] + } + }, + "timestamp": "2024-06-27T12:34:56Z" +} +``` + +## Subscription Extensions + +Extensions are a way for clients to define if the server shall enable/disable some features like enriching data that it publishes. When serving data on-chain it may reference off-chain data or past on-chain data +which is more complex to track. To simplify the client-side consumption of such events, the server can aggregate or pull data from other datasources to enrich the +events it serves to clients at their discretion. To switch extensions on/off, the client provides an array of choices. +Setting extension's config value to `false` will switch it off if the extension is on by default. + +### Resolving transaction inputs + +For UTxO blockchains like Bitcoin or Cardano, transaction inputs have the following structure: + +```json +{ + "outputIndex": { + "type": "number" + }, + "txHash": { + "type": "string" + } +} +``` + +When setting the `resolveTxInput` extension config to `true`, the server will include the transaction input address that was spent. + +### Asset Metadata + +[!TODO] + +## Subscription Object + +The subscription object is blockchain specific, because there are different networks, credentials or other fields required. +Chain specific fields are grouped under a specific field named after the given chain. +In the case of Cardano blockchain, all the relevant fields are placed under a field named `cardano`. +Below we list the currently supported subscriptions: + +### Cardano + +This is the format for a Cardano specific object in the `topic` object. A client can subscribe to multiple topics one +after another at any given time. A client can subscribe to multiple accounts by providing multiple credentials in the +`credentials` array. +The client can provide multiple starting `points` in the mandatory `points` array. +The first valid point provided in the array will be used as the starting point. If all the points are invalid, the +subscription will fail. +In case of an empty array, starting point will be the genesis. + +```json +{ + "blockchain": { + "name": "cardano", + "network": { + "type": "string", + "enum": ["mainnet", "preprod", "preview"] + } + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + }, + "signature": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + }, + "cardano": { + "type": "object", + "properties": { + "credentials": { + "type": "object", + "properties": { + "payment": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + } + }, + "stake": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + } + } + }, + "required": ["payment", "stake"] + }, + "points": { + "type": "array", + "properties": { + "slot": { + "type": "integer", + "minimum": 0 + }, + "hash": { + "type": "string", + "pattern": "^[A-Za-z0-9+/=]*$" + } + }, + "required": ["slot", "hash"] + }, + "extensions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": {}, + "version": { + "type": "string" + } + }, + "required": ["name", "config", "version"] + } + } + }, + "required": ["credentials", "points", "extensions"] + }, + "required": ["blockchain", "signature", "cardano"] +} +``` + +### Other blockchains + +Any other blockchain schema should follow the same structure as the above Cardano example by applying these: + +- Group blockchain specific properties under one property named after the blockchain (see `cardano` field) +- Keep chain specific authentication fields under `credentials` property +- Keep chain specific configuration and extensions under `extensions` property +- Keep chain specific starting point fields in `points` array. If not supporting multiple points, use a `point` object. +- Define a JSON Schema in the documentation diff --git a/docs/messages/server/cardano/era-summary.md b/docs/CIP/CIP-XXXX/messages/server/era-summary.md similarity index 100% rename from docs/messages/server/cardano/era-summary.md rename to docs/CIP/CIP-XXXX/messages/server/era-summary.md diff --git a/docs/messages/server/genesis.md b/docs/CIP/CIP-XXXX/messages/server/genesis.md similarity index 100% rename from docs/messages/server/genesis.md rename to docs/CIP/CIP-XXXX/messages/server/genesis.md diff --git a/docs/messages/server/cardano/protocol-parameters.md b/docs/CIP/CIP-XXXX/messages/server/protocol-parameters.md similarity index 100% rename from docs/messages/server/cardano/protocol-parameters.md rename to docs/CIP/CIP-XXXX/messages/server/protocol-parameters.md diff --git a/docs/CIP/CIP-XXXX/messages/server/rewards.md b/docs/CIP/CIP-XXXX/messages/server/rewards.md new file mode 100644 index 0000000..1fac486 --- /dev/null +++ b/docs/CIP/CIP-XXXX/messages/server/rewards.md @@ -0,0 +1,118 @@ +# Rewards Server Message Partial + +This top level message key is added by the server as part of any synchronization process for every epoch boundary transition. +In respect to the Cardano blockchain, there are two kinds of rewards: + +## 1. Staking Rewards + +Staking rewards originate from two sources: + +### Reserve/ Monetary expansion + +The reserve is a fixed amount of ADA set aside at the blockchain's creation that is gradually distributed over time every epoch. + +### Transaction Fees + +A portion of fees is collected from network transactions. + +> [!NOTE] +> More details can be found [here](https://docs.cardano.org/about-cardano/explore-more/monetary-policy/#monetary-policy). + +## 2. Voting Rewards + +Voting rewards for participating in Catalyst are funded by the treasury which receives a percentage of transaction fees (defined by a protocol parameter). +The treasury is primarily used for funding development and improvements to the Cardano ecosystem but has also distributed rewards through community voting in the past. + +> [!NOTE] +> **Withdrawals** +> +> By default, any accrued rewards are a credit in a special rewards account associated to the respective wallet until withdrawn. This rewards account uses a balance model. > A wallet's balance already includes rewards as soon as they are granted. In order for a wallet to claim/ spend aggregated rewards, the user must initiate a withdrawal > > transaction, which moves the rewards from the rewards account to the wallet, creating new UTxOs. +> + +## Schema + +To enable wallets to distinguish between both kind of rewards, a reward message contains its source type (delegation/ voting). + +```json +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "rewards": { + "type": "object", + "properties": { + "epoch": { + "type": "integer", + "minimum": 0 + }, + "delegation": { + "type": "object", + "properties": { + "poolId": { + "type": "string", + "pattern": "^pool[a-zA-Z0-9]{54}$" + }, + "lovelace": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["poolId", "lovelace"] + }, + "voting": { + "type": "object", + "properties": { + "lovelace": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["lovelace"] + }, + "poolDeposit": { + "type": "object", + "properties": { + "poolId": { + "type": "string", + "pattern": "^pool[a-zA-Z0-9]{54}$" + }, + "lovelace": { + "type": "integer", + "minimum": 0 + } + }, + "required": ["lovelace"] + } + }, + "required": ["epoch"], + "anyOf": [ + { "required": ["delegation"] }, + { "required": ["voting"] }, + { "required": ["poolDeposit"] } + ] + } + }, + "required": ["rewards"] +} +``` + +## Example + +```json +{ + "rewards": { + "epoch": 223, + "delegation": { + "poolId": "pool13gdtqme63jprkug3j4wzslhmu0yk4kdx323rtxpjuz7rqv3yyes", + "lovelace": 1234567 + }, + "voting": { + "lovelace": 111222 + }, + "poolDeposit": { + "poolId": "pool13gdtqme63jprkug3j4wzslhmu0yk4kdx323rtxpjuz7rqv3yyes", + "lovelace": 500000000 + } + } +} +``` diff --git a/docs/messages/server/tip.md b/docs/CIP/CIP-XXXX/messages/server/tip.md similarity index 100% rename from docs/messages/server/tip.md rename to docs/CIP/CIP-XXXX/messages/server/tip.md diff --git a/docs/messages/server/transaction.md b/docs/CIP/CIP-XXXX/messages/server/transaction.md similarity index 100% rename from docs/messages/server/transaction.md rename to docs/CIP/CIP-XXXX/messages/server/transaction.md diff --git a/docs/CIP/CIP-XXXX/messages/server/welcome.md b/docs/CIP/CIP-XXXX/messages/server/welcome.md new file mode 100644 index 0000000..7689466 --- /dev/null +++ b/docs/CIP/CIP-XXXX/messages/server/welcome.md @@ -0,0 +1,125 @@ +# Welcome Message Partial + +This message is sent to all clients as the first message immediately upon establishing a connection. +It contains essential details about the server and API, helping clients understand the environment they are interacting +with. + +The message can contain: + +- A list of supported blockchains, networks and extensions +- Versioning information + +## Schema + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Schema for Welcome message partial", + "type": "object", + "properties": { + "welcome": { + "type": "object", + "properties": { + "blockchains": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "network": { + "type": "string" + }, + "extensions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": {}, // using 'false' will switch off the extension + "default": {}, + "switchable": { + "type": "boolean" + }, + "versions": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "name", + "config", + "default", + "switchable", + "versions" + ] + } + }, + "version": { + "type": "string" + } + }, + "required": [ + "name", + "network", + "extensions", + "version" + ] + } + } + }, + "required": [ + "blockchains" + ] + } + }, + "required": [ + "welcome" + ] +} +``` + +### Message Example + +This message will contain details for all the served blockchains and networks in the API. + +```json +{ + "welcome": { + "blockchains": [ + { + "name": "cardano", + "network": "mainnet", + "extensions": [ + { + "name": "assetMetadata", + "config": false, + "switchable": true, + "versions": [ + "1.0", + "1.1" + ] + }, + { + "name": "CIP-XXXX", + "config": { + "timeout": 10 + }, + "switchable": false, + "versions": [ + "1.0" + ] + } + ], + "uri": "wss://www.example.com/socketserver", + "version": "0.1" + } + ] + } +} +``` diff --git a/docs/02-Synchronization.puml b/docs/CIP/CIP-XXXX/src/02-Synchronization-Sequence.puml similarity index 82% rename from docs/02-Synchronization.puml rename to docs/CIP/CIP-XXXX/src/02-Synchronization-Sequence.puml index eed5d7c..32c2fda 100644 --- a/docs/02-Synchronization.puml +++ b/docs/CIP/CIP-XXXX/src/02-Synchronization-Sequence.puml @@ -9,15 +9,21 @@ queue "Task Queue" as Queue participant "Projection Worker" as Worker group Wallet Connects - Wallet -> Server: connect(credentials, points: PointOrOrigin[]) + Server -> Wallet: welcome {service details} + Wallet -> Server: subscribe(credentials, points: PointOrOrigin[]) Server -> DB: findIntersection(points) DB -> Server: response: intersection point or genesis + alt if no intersection found + Server -> Wallet: { invalid point error } + Wallet --> Server: subscribe(points: PointOrOrigin[]) + end + alt if point is genesis Server -> Wallet: {network genesis parameters} end - group Catch-up Phase: Get intersection events + group Replay Phase: Get intersection events group protocol parameters Server -> DB: protocolParametersSince(point) DB --> Server: response: protocolParameters: [protocol-parameters] @@ -60,7 +66,7 @@ group Wallet Connects end -group Wallet Synchronization +group Follow Tip Node -> Projector: Chain Sync Projector -> DB: write & notify(extendedBlock) DB -> Server: notify(extendedBlock) @@ -75,14 +81,4 @@ group Wallet Synchronization end end -group Asynchronous projection - Node -> Projector: Chain Sync - Projector -> Queue: insert or delete task - Worker -> Queue: getTask() - Queue --> Worker: return task - Worker -> DB: insert & notify - DB -> Server: {metadata: {pool:{id, metadata}} - Server -> Wallet: {metadata: {pool:{id, metadata}} -end - @enduml diff --git a/docs/CPS/CPS-XXXX/README.md b/docs/CPS/CPS-XXXX/README.md new file mode 100644 index 0000000..45b296e --- /dev/null +++ b/docs/CPS/CPS-XXXX/README.md @@ -0,0 +1,132 @@ +--- +CPS: +Title: Sub-Optimal Provider APIs for Wallets +Status: Open +Category: Wallets +Authors: + - Rhys Bartels-Waller + - William Wolff + - Martynas Kazlauskas + - Daniele Ricci + - Hızır Sefa İrken + +Proposed Solutions: [] +Discussions: + - https://github.com/cardano-foundation/CIPs/tree/master/CPS-0011 + - https://cardano.stackexchange.com/questions/4614/best-practice-for-handling-rollbacks +Created: 2024-07-09 +License: CC-BY-4.0 +--- + +## Abstract +The event-based nature of a Blockchain is well-suited for wallets to derive their internal state from genesis, however, most data services only expose a request/response interface and/or take on the responsibility of decoding the native binary data into JSON, enforcing a strong client/server contract with the current protocol transaction format. Wallets must use polling to detect changes, on an interval that balances latency with network and service demand, then make numerous calls against various endpoints that have the potential to change on each protocol upgrade. Rollback handling is either non-existent or rudimentary, requiring defensive workarounds to detect and act accordingly within the protocol rules. Furthermore, the lack of an API standard inhibits development and adoption of a standalone desktop service for wallets to offer a full node experience for their users, which is best achieved via a universal API running either on a remote server, in a multi-tenant mode, or locally running in single-tenant mode. + +## Problem +> [!NOTE] +> +> Fundamental wallet functionality is defined in this document as being able to construct and submit any transaction supported by the latest Cardano protocols, requiring: +> - **UTxO set** owned by addresses with credentials controlled by the wallet, that are either derived within the 1852 purposed, [BIP32-ed25519](https://input-output-hk.github.io/adrestia/static/Ed25519_BIP.pdf) accounts or the product of hashing a script. +> - **Registered stake pools**: to verify a nominated Stake Pools ID is valid when delegating stake. +> - **Registered DReps**: to verify a nominated DRep is valid when delegating voting rights. +> - **Reward account balances**: to fund the transaction with rewards earned by the stake key registration +> - **Certificates submitted by the wallet**: for understanding stake delegation, voting preferences, and any future extension to the protocol. +> - **Current Cardano network protocol parameters**: for building compliant transactions +> - **Current chain tip**: for setting validity interval and/or creating time-locked scripts + +### Impedance Mismatch +- The current APIs available to light wallets tend to be part of a general purpose solution serving many application types, built on top of [Cardano DB Sync] with a request/response interaction style. To achieve a reactive UX and stay in sync with the network state, polling is required to detect progression of the network state and then check if there are any updates relevant to the wallet. i.e. It takes more work than necessary to maintain the event-based nature of the system. + +> [!NOTE] +> +> - Wallets require a local node to create a trustless connection with the network for fundamental behaviour, however, the current node technology renders it impractical for most users. +> - While current technology is a limiting aspect today, we can use this as inspiration and guidance to determine a solution that satisfies some of the desirable properties. + +### Fragile API +- The API contract between a wallet and most data providers is unnecessarily complex, increasing the maintenance burden and transmission footprint. +- Wallets can easily deserialize CBOR, using readily available client libraries. +- Using Mithril to validate data requires an agreed standard + +> [!WARNING] +> +> This fragility in the API creates friction around each protocol upgrade, with wallets needing to synchronise development effort, which often goes through multiple iterations over the course of months. + +### Lacking a Standard +- Wallet users, particularly light wallets, have little control over the data source, and subsequently must trust the product team's decisions. +- Users cannot choose their own data provider, independent of the wallet software, which could be self-hosted or from an alternative vendor. +- This complicates or inhibits products offering a full node experience, with a free and open source service, available for users to install then choose their wallet of choice. + +### Rudimentary Rollback Handling +- The Cardano blockchain has the potential to roll back blocks as part of resolving a chain fork event. +- Wallets electing to store data fetched from the provider locally must implement a strategy for handling the rollback scenario, which is specific to the application and therefore a responsibility of the client to handle. +- An API querying data from a store is unaware of the rollback occurring, so the client must implement a heavy-handed approach by continually testing the tip meets expectations. +- The result of this approach is a varying level of quality of outcomes in wallets, plus extra complexity and network demand. + +> [!NOTE] +> See [What is a rollback](https://plutus-apps.readthedocs.io/en/latest/plutus/explanations/rollback.html) and [CIP-9](https://cips.cardano.org/cip/CIP-9) for detail on `securityParam`. + +### Inefficient Address Discovery +- Scanning a [BIP-44](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki#account-discovery) keychain for used addresses has the potential to be quite an intensive operation and is non-trivial to implement in the wallet. +- Most APIs support query by address only, missing the simplified alternative approach of using payment and stake key credentials. Funds intentionally or inadvertently sent to an address that would otherwise not be discovered under BIP-44 discovery standards would be found with this approach. +- Funds under the control of unknown [Franken Address](https://www.essentialcardano.io/glossary/franken-address) go undetected, despite the wallet having part ownership. + +## Use cases + +### Lace Extension + +#### What is it doing? +- Transaction data in responses are **transmitted as JSON**, via a **custom schema** +- Data is sourced from the combination of [Cardano DB Sync], Ouroboros Local State Queries, and custom Ouroboros Chain-Sync projections into the PostgreSQL DB. +- **Polling the tip for changes**. An HTTP GET request is made to a webserver every 5 seconds, checking if there's a new tip in the [Cardano DB Sync] database. If so, requests are then made to fetch transactions that are associated with the provided list of addresses after a certain point. +- As there is **no rollback support** in the API, rather than just fetching new data, the last known point's data must be fetched again, so it can be compared with the local state to infer if a rollback has likely occurred. If it has, the state associated with the now void blocks is removed from the stores, then a new request made with the new local tip. This process repeats until an intersection is found with the server and syncing continues as per normal. The wallet ends up requesting the same data repeatedly, which is inefficient and places a higher demand on the DB with unique queries that cannot be practically cached in memory. + + +> [!NOTE] +> Requests are also made to fetch data on-demand but the scope of this document is to focus on the fundamental wallet requirements, so it's not mentioned here. + +Lace fundamental state sequence flow diagram + +#### Why? +- The philosophy in Lace is to place the responsibility of deriving wallet state from the historical transactions, similar to projecting state via the chain-sync protocol. +- Blockfrost was used during the early development of the Lace frontend, however the design of the API to require many small requests conflicts with the rollback handling strategy of Lace, and therefore was not a good fit for fetching transactions. +- A custom API was implemented for this data access, with a batch interface, to query the `cardano-db-sync` database, and later custom projections with more a more optimised schema. +- The API schema was implemented from scratch, based on the domain model, due to the lack of an existing standard. + +> [!NOTE] +> Since this API was developed, [Cardano DB Sync] has since added a mode to store transactions in CBOR, but at the time of implementation it was not an option. + +#### Current Alternatives +[Blockfrost WebSocket Link] is a partial solution for the impedance mismatch problem, however it's not addressing the fragility and standards problem. It's not practical to run this service to satisfy a single-tenant, full node, use case. + +## Goals +- Define a standard API that is optimal for fundamental wallet operation. Consider extensibility for optional enhancement. +- Deliver data as a filtered subset of the network state, using native encoding formats, to simplify server implementations and push processing to the edge of the network. +- Foster long term stability of the API when crossing major Cardano protocol upgrade boundaries. +- Facilitate event-driven wallet behavior, minimising the need for polling, and to increase the sophistication of infrastructure management and user-base insights. +- Support script-based wallets +- Minimise the network demand by allowing the server to control the throttling strategy, rather than a pull-based approach like we see in the local chain-sync protocol. +- API can be applied in a local, single-tenant environment, and a remote multi-tenant deployment, to facilitate a universal approach to wallet supporting an optional full local node for power users. +- Design that supports mobile use cases where a persistent connection is not always possible or preferable. +- Remove or reduce the need for wallets to perform address discovery + +## Open Questions +- What identifiers will wallets provide for the server to filter on? +- What is the most efficient solution for indexing data based on account identifiers? +- Will it support multiple accounts? +- How will chain rollbacks be handled? +- What design considerations are made to establish a stable API over network protocol upgrades? +- How will initial sync be made time and network efficient? Can snapshots from the immutable part of the chain be utilised? Is there an advantage to sending batch data in messages? +- Will the API standard support extensions? +- Should verification of stake and voting rights delegation be considered part of fundamental wallet behaviour or an extension? +- Can Mithril be used to verify state snapshots for provided account credentials, to facilitate a fast catchup feature? If not, what is a fallback approach for resource and/or network-constrained applications? +- How will the API behaviour differ for single-tenant use cases? +- Are there any natural authentication strategies? +- Should the Block be transmitted to wallet, similar to the Ouroboros Chain-Sync protocol, or just the relevant transactions? +- Will this API be applicable for other Blockchains? +- Can push notifications be leveraged to address background mobile use cases more optimally? +- Will connections be cheap enough for the client and server to support frequent reconnections from a low quality network scenario? + +## Copyright +This CPS is licensed under [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/legalcode). + +[Cardano DB Sync]: https://github.com/IntersectMBO/cardano-db-sync +[Blockfrost WebSocket Link]: https://github.com/blockfrost/blockfrost-websocket-link diff --git a/docs/CPS/CPS-XXXX/images/Lace_Fundamental_State_Sequence_Flow.png b/docs/CPS/CPS-XXXX/images/Lace_Fundamental_State_Sequence_Flow.png new file mode 100644 index 0000000000000000000000000000000000000000..3db7abbc8df8a203ca73d1dea1c916acde0e1738 GIT binary patch literal 96319 zcmdqJbyU?`)HQrm5X3+fM3hhvIDmj6-Ho7hDJ@D1NJ}G#l!CN$cPbza7J_tlNOyO? z>xkZ~SMPnEFaG$(cgGz=h4YKO_u6aCHRs&NLrOyAB-RBi1OjnV^roN;0)f_!K%hUs zJPbbx)k_tv7u#sH3}#l12+s)q@aJVQw+|H!P^n;E@$C00e7!4IF&#a8Ilv@%2A$z)E(6OB zjcxjs$@^qX^8DRj5%Y`-60xU)_$kQrq%S_tkYcC`SA1gOuB2;n_zGI}(}){lJIEFu zsR$g}>Yauh$!nGxbZ4t_J)6$AdXrmQBHfH;RSIF&YHvdJB+O#_l z+a6@zPACcV$Ll&4dBQuddn^T=Jjw*$Bi63&rGlAW0e;oW`nqWBB%1DF37X|&&!Z!w zi1Ca>(*|V?p1egeMcywG|HMzG?sA7atwrDG!OC@!X`ITBxVY<|M=qj?chfyCnR|bN zMc<1zA?`Y}!*kUTMx|1>i0pZ}EuxLsbfD9MM{Ge?Lkse{{h@`82H%MLxn zRP`f1;xKKg{Jiq)?lAA3SV5e+jm`P6M04YEiO;-p&x(jLZ90NG@=JR(>S=p*F3^>~ zYa3q}qbMnRy2p7~XD(ZV!Sh|Rifs7P`qTYi#}nQ zk+!b&oJj+%Y;ITv-QA9>rTR*lk80AiL(mn?ShCD%^R5W>vcCH2T<~CSVZXvQY@gMv z9-Eb6p}ux~xx}X-d_7_|+^XKTKT9V@@Dyw__>b2Y8Iy7FSH!iWq{xH6;^GKO9sCVR z>K=XYHz~ndTt0t)Z2N`xZnBBDlNb!Ui*s}uBt%7qngD?Ju1{hjQQI+g);>!o2r*90-I;&{Mj3uRlY4^8*;(-E9*-($9inn z=h}>p9(>Brm&-XZF;RN2l~U3|^cKf{yYl?k((iBfMU;pyC&!H9_C{FRI!_xp)9Q?} zacM#y@xiqZNG;I%^6B;FdMmcxP3B%zy_$VbEL`axa19Nkie@qU8KfCjg>i0LJ z@r8dBM;~0F0Pj&jKZHt>2*e$aRvcVMeB5vUcqbO`9ess~iIzh94Yyh9Ev4?w{C=D9 zkuUz;FHlJkapKWrGr8MG-&e0TcGm}5%5!fsbq6Be5p6|SGUBUiR=lOGP5u;t7~71w(2@cfB3o}EWYzGNb_%5P`BzIW+XMex#{ z6_gr!6#j})Zz-HDD%CNSexZ=!zLj(w^m-;m44SNDQa%J znkK7ky(aJvZ? zZ0>Ldt*xAfwJ7GUmEsV*)vL&$&DcWx}uuEUgd7aLvZf`eb zOLBybHkTqpQn~4EuG_Pd7waQ9=DYI@x<4X1{X$5S4?FIk{WeW+OooH1s)YIY_!_wI zcPsGsCB*BR-D>U5bqB5SZpV}|8TCHcUYqT9In;@L5aI9k?H1mPJbCg2LU5uzqj=S~ zd6wTc&b~Qv(Q4+;(47lQsLsrwSEy_wU`i zM<(LkN=*EY`QTaa<}%gGiO-y2c-X0HuX}ZcLG#nA#zraj*tIK&nL{|=;$_BYzQ1&D zZ!bsNK3(#;s~Y#t!hQC3`tqGvu44$>mo(0qcU}+e+D$aYxxK9>=nh*$y&M9;9_6q_ zRq-K3c0Ut!)dbQ!o%~jiuH@lP>3n=9P?=;-hBH^*f^ zM&(jz#3(tI_PfcH9I2#7q~zb;F^`6WdkNzp zATBjvrcz;?+6j8~YGt(}O!w?oa8QbHd1wpF9JAZ^nNw`Uw4p@D!F_n@dlbQ+(N&ke z4;luKOYmb9f>KVTF5fSM%cUOth)wv@^Tff%=_DdW^7>v^N826t;jvkt6W`arh+17l zFFaK0!R>Q=#(hPr-3D3YgXKif#=sC#`i|BB3cuCLf?uD7teTSZPXqgx4qi0DxJm|Kv1 zeE-F<6?YR?$B`G$A_EI?=kqS#5R}5j6>QuO4hmXZ>oGPkkWUV5Z)!h`=sndk6idJJ z0G~@!Wst-=E=EU-uEN%^>I4ohIy%mZBb`dYHNEEe>5eRei*j%c)tjt_!%m|2lvGw+ zqh?}7p404*88fb=*Css9IvGMvK_PQhrIcN5KN`KyiwV8%gaRw4YOD0@a+dFizje~vG%_$s2Nqifq-^LmOA#aV?@jkbNBdYRv2tn zYGPtygqHh{ks`(Rj*}*Hl;8NYqmP3?{P_)fZ_$_HUbyhq(J^(;gyxf9z(~P(q^+c@l()V}VRoVTCzI!qDke-7e#WoRlUXvi3cygqi3O`kNpsZoKLlJ+NH2LvXWBF9|WdUIs zaxT7=X#=tCwFtGx_uR|48~hp$eJb6(xO-bCk-!LdTewlc?Ns09*PFJ{ySfI4-u0`9 zdrh`w^0^3yvFv9%?AA}V=vdnX#ZkS{j(}7=%~JBwuc3p}qA5za*b=)Voq*GE@6CK! z4{YNilVP<8HnT_vfy9aRNnw+`vkshAZDas)E+LbT22Qr{>`bN2m+hE-e0t13O@3Kk za%K6dgyfeOmA$3>_;{)}@u=lCCX)nWkK(l63yj43Sn>Hqu|npKW$mk1o_I7&25U}B z`wG=qa=NZTw94BXawazQR(m5BS+G0rVVRBBpFDBGZevg+->5Ic5z$B2lfS>xc|St0 zL1-uAdJ@jc#-iEcVvi63EOiKjR$!QiNBI;?mjIcrhKAJU(kRtsuL~Clk#C!#@AYHa zj5nAk9dYNYNW@uLnQX}_UF*(J&d)uGE(jCV*q?h6p>aPmv0^2(Beue&x~|MVDoOFU z$XwbCgmJ6bjVcV{$5p=D4A&kn+*&ccnbp2NK(e_xZcstdl4viLr*E;!xaC1aerIIB0*-UTykt@x%}+zG1jF}h)7=%l3B6Wg3A`^rM5neUm8DDlF%EsCZ_}| z3b7kidiyx+?{aGq?NVL$VJjyI)oBcb*I{D!AN*M9DZrtkek7ECys^k)dtqUFYt>e| zHJJ>Vb9U@$NLeHPo-a#io~;6xij?ThPHEUU9%l#4i$07;Ig0O=Sxozc?p~8ro4U(h z8Yj|#WKu1*bewrzNi?(B@?qqQtU<@AnC125WfF!5@eU2Ks5Lv+lW%O?94`uhV7WmA z1qcEwyI%13IIOm9O-x+=1|;s^SoHOjtSQq z%-yE9hZ;}X)YjzMgbwNxYMhFvVtx_rg-^c+B{VnBC0Y4XUZU}t(T~{pQuq&h*3OFW7Mpt% zt@pbRS2lQ1d|z3ch(tVZqMDt#VpYbE(c5&PDym09NhxG%g0m90-)PlD8OQ&=v}gHJ z%qJFfKR$c0O0%yfUnwxj`U5dKx%o|T5P83h-741Li~e9gKIBlhP3td6#yXxDw5fpoM;->d(COk zUb%AN68Ed566VGl>{0{`NqLNd&D}Fu)~$kB-z{(F{@{uvfV4AUqRs!?b*mS z5vfq`mSaeofU9@?)`VT>=()Jc_iX$fcusXls)@|$T-SX=6EtAAtcvw|$krpXK59<8 z&~)@wX9^0KysCe3k>$*pGtaO|)bfo~_f=?>I2)51a_Gwmm)FZ1Vova$wWn5hC>$L2 z39b*{^zk^WZoAgCht0X*bSLEPg&W$nu)4Y1wERYbQhNl88b)aL!!ITS9zd#AkXyPR zkakN^_ES|zz)1A8bxlwoqlU~}cOH$XNA!;LE9~l2x5N1uFYV^kWmULZHn(FPT`jg4 zXs0R|Ken;HBe^My0=QTqEqbqqvm`qjy+l%E5_F%a-Cvtwo2jxtBvsz|u(yC_J%?pq zUSBt~GgY4BHB_KxEIjJQ;|SV(!=A;-q$s@AYJ-o^w-y#|EkB;^=DMo3JT#n76*Vi3 zhS6~@O;18V9SZ2J#j!TUfkgk9;)VS3U2B%(MUqM5>WvW-$9cLey2*|m;}FZ{b0z-b zO+=DFa728ig35!%ov=t&lX#hP{RSlVYK2x8AMWlp-wvi|Ix#fWe4`t)`)OHa(Cna6 z=H_21v;8g_kvt@QE%ediXxv*9DS|nYit3qWTO$cebu7xdg59OtmuTJG2G*Nz4GWg0 zEhmZ6M(ihwdu$0z+*(w-m}t51_5El~uxz~DN|Q*SU@G5gtJrj7Xl|J$e#QG2nAJRF z6YM82G2_K`Rb&*tRw5OHGZT?yI&yMZ_db5U9lr6bAO+S68R|Zvk9=f{_%gn?S}}Pe zqMUw9@lFzb%c-{CW}j@GX?|~;jOF15c4@EYdsW;;lG1ANMZc@ua#UEQS7`QqUitY+ zzt&9X%HiqCp-cdbALY20m)rped1oA7%S<0O3u9kAMbf_QRAaeqe1% zWg{QEPT%N|jG-qWqSoP5ggi?{?ZeX_XT2O0iaBLYS$k~RK87BJqz%^7xt*IoF*3aI zXu+F6qgGs#-e-Q37RzZ}Aubkq{d!hqrEyZ%tT&OZP(s==ez2Rv)MPHI9+3yrshYA9 zdU$vYIPP=p-l~y{Qx!dIBqF8QfLU~k!(vLNv}jqOa)j*qJ+Nlf$> zdJ(}+B@rUR35j_(d+18k_V}&AvfCVTEhiL*5ssI{p&>>fAD|LaUw|}KC_ZBG{gu&M zJ3}s_^kuAacLyPI6FQPu2JBcaU6R`5cVFs$G|?CpMl%o+Q|IDh;MKvoI2f`xtS)Pr zK)2O#Iq`PL(z;B77!l{=<`?!;4FfMCbOAJ5w2?6urYPkVjd3V$uMs6+&|Y6uw4v&I zJ4-1|#oaSWt#QrA`pH^cZO~H5(STU*=$`!fn3Q9)*-xij3M8yPgkKxL-$Hd1IhGCj z&yI_EFcO6`PSPgPxMQ-LPat>fVSi<#h5W+X zWJcz^)PQM1B-5IrP^akeVCVekRC61y)<`^Ad$P1wB*&0#s~Fm-&(cJk>Le9e(695g zw8*Ej#~Z5Yg~_h*&WYKu8*nM&`Jld1dX%hZtmC0*>no*0bV@6QOUox9P||Z9_BNG5 zJ)XI?$x5;Wb{wT%ks-oq;@#OBhrKvEI2dEMqyyQyc&^AxO%b~Ih?m(eYO(!x%Z~m> z0ZqATAIlZ6DZN|Des6axg2e7x)Y!UVkEOZs_c+CcI87C?oE3}h=!lLEZAo%0u$=u& zU*3EA>~hgY3z3LV-UTaY;s0%v8!M zm`Zjn&gUF;VC|;Q`NcQEiWLMmw2cSK#;3Dbq@CLcrKw=j&p%BJAUAEgqyU7>t5>gR z6f<=dtKH~Si=JO#5>vLV1fGt?bfoI*R}D!?NvO#Z5$uMY*?fAtv-tx@v55(Bape+k z6Kt`tvxn*jv~xt!7US>{&D`KGJjFy6K=2J?wUUizl}B!@b}+t9-(w>kLpC*ioa_=A z7bQEwRq-n3UFo4K``e0G`?q!Gyc`#faxa>m{O=m9vn5Tal@W|Q4pyl`oR4}Vwmo0gw%b#ENDRg+}%C8QT*;9@R zh(LY{Hs_WjxS(VOjCf5j{XDe>YiA|r{ zETPsIHk#GLbsTyONqa}AtFfMrIj^jkZ>@H4MwQ6ftW1b!h!t35k4xT|6!q_q#dvEz z$pAfJtyvDV_AZS9Z}eIcF-{R~Y>r3q>}?F2j#j@QxFK9D!(D*Sx4aY4Q%P)_ZaLeX zrjXHH?zlfz7dGIqJqy&Jdtz&AtCN!x5U$XX28)V{sypsl#0kGhel}HsWyP&*N$(86 zc-5l7WSFj6uKHqp=4a$svpjxXRSK>JnneM3wd3CMtsHG^sdj4LZ4ueOmP;(M!CY}& zh-bp1bW%OBt>uI^;UkMn;;#W?m)cl(ouh?Gm&?;{t0ZyU0UA?G*3xpVHsr>~4rJ&ZI`ZtJ$?)gR?d_J5*P{Lu7g>zt+C6-HlCniiVquDa9_Mt} zDOgfS<$bcYG+G1ju_QQH5nhTyJanb@MnlQQkeBB2DI)e0B;4W5F< zl}-%~4o=ad3kwNtZf>dqBb8EUK6yr{5y(o9uuScm&0rlgAwFr(3^Lt^4`+ha9YXy4 z-dN4|tCiXSJpJ09p&UDD?6`eVsKxa-zRWc2BAtdv+9^uO$lDE@7Srw0YN~nqcrPem zzwPXE9Y$ydX)YMLBY>4Z0l{3#3li6K(!LjoPe&HxdYMXrD-6ReC%csF78DUt zOur*d#8(qQ1r1M>R5e~X|EF4^Ny4qQ*=<-Xr0qF6wUV2YD;fFy&8;mS(z~0><7Vbo zbtm1Uk(=~yy`%MECnZu20$EKOq*o%PndBVV3>ag7y?j5TwPf-0MIX*3cYQnr;Gl+|R&hi4BtwLaH(D`~L;UmdA@Ea3jFoz22s9>to5haqm5o__n9 z)0jn$`+G3>5}Vbj>Zq!BRwT5y?}6^+~;9l41W3Ybe;acQC}Y~9Wmt*m|pCN zvP!&Sy|pqKAAdzjVc(?wy$c4<&ca8wLX%w3!f-Fd#l~tkL`FnL(kV!xuz2$ru0mj4 zvE^*4cl6Zyt>thp8Rxz-`)%9#QV~jCN-7&Z!A4-8k!iDUKfM z<$aMQQ>D-pSF8fV1YXXt4hsN&bQgWzSivh=d`uMO=Vec(tE6;+%S~2B=2M;h_8Qeb z&j^5pScmEWs!$sQf(b&T?B@{q#+cqaog^y;wR}|P$tMgG5g%gvZ6!zj{*uBC^giHS zea>I!mmAyUQ?Wp!ZB#J_7B)dFa0^5=y-g3w$c6QFd_CD{8+Iw!#h=8JtTK2HsKYaS zkRM7!Myq_uV5-&^7ekl~&Sks|3F*pI<**5&CXoj53{X)Z&~W}B`Rp%z#NOYiLS#G!$Ot-qaY7h9nLSe|~4WnN3NSJY1 zOx&l+N*06mbnUuONIJMQtP!*{7o#e!0!!%O^7@sk}sK067y*H}YCV|Qzn zXL}|GIwB+vBL|1==jQ|#Q>{3TrHZ>FNvvYZfsjw(tr+R9T)Bc2MM8Xs~)BZt7;-Ra^!RtGTOF(5mjj-fBMKKjlNh)1N9&Bj!iG zw-tA_fonZHI2d266#_D4fUHfVkr3ak34{ajNX`)F1RozsXhZq=`H>Tm999xto|+5M zd@j2EG-XxYKuOfr-cD$1d&sumZw+z^d3nV-6l_3>NA5`I>jyc!6S|%}(_?kBo9 zn@`bx6$Y6;H34pF2HAeHP zsi~Fku7z19e0U4VB}F!AYTrG%C%LlnxivZ!E_uTl{Cs_so_W0LJcwu-WKhCGx$TdN z;RHb5RWEl??K#9g;mc3IY4=T0$i8W6_^K+B%k~1Jt`r1ibs$Z?LB|}F&gRT}XIkfh z&1_LWgG7p1Z_iTED5N*4u|K=6g|-V?O`bdg(eBD8d2|B3hri>-Buu1$PFlxiqX*l2uRx<+NL zvd9lj%1ff7Tm;eh509&2pHLyDaP_89OMD)^ario-6YEdE`KC4@G=#q^s|YE<1DOhe zh;~1awZPPX1Mgpf?|$f=!f(%HfZsGAk2qy$H2NR1dH=R?_1(MCdomLRLnyKF6373u z5uxt>ug?V?lwb$8-^ZBb+Y#)mM?iEPd&HXS!r}BDHHLTM0bU?5IfdA747zo{CwixW z8sk?A96s0@u^6{o zA`tJ0Hom%GpXQ6X^NPWF==dju22JxR8_+G%00MA?^Ej5heKOjwK;ew3R1$B0*!je5 zd+Sa%=g^|ztM3AEaCzcRAl{PBMvtqZ?V{4pOin{PK!S%|Z`xAj4cb!MU=s=Y+umCO zCxQ`VomBa>+>`mYBqZhr${jT{E)nJHx25{}`ubCdH)mcU7i=6NpFMl}fyPwL$9kOB zRwA84sL{Rgw`_zC4p>-4t3C^S!_mpfF4o}S;0`h$PEw^lujoy|yTO)fu#=%TgFFQp z%FR1dRaDG9y5d$h<)x~3t-%6Yp^$aHvAVP|+E0T&f`jXM{M&AbCtAU}z(=JYRJFQTNn+4r>QOa5IM@X92)IFM zZEY>_xtMre7z=nSl91NIA|kR=afvXXw0uA=o+-H(Muk_2on2fewfl-JDxVYNJow@l z5PcRoZXGG-LyOs_y-!Gu{_ zT3RLtzzY<~Y7#3M#Vt2bYHPKsFoo*jrIh;19m{R7!8GkIf+> zC2JLq4zG6@;dBBfaCwt1`LS)w;3M}yNSZyhwY7ln+mfXP>|QB6o``X4RZbzB;MrTm zt|JZ*xI#{Dzcrb}WwUZx912}bH2}#b2J|K=yWT6*AX&S*S!keY*Y5Qc48SsfLCky(4Svz4D+knlJl`5XU$|txp@su`#YL zj)vWP&fvl#9TN3ytnbnC?a=|S{hs+f@aP@b28mZRN4pOCrI8hUlQv&tP`$}b%QH8d zL`X{a_qOq96s}4yU_wX2hfxZ(8k!Uu2fP%1SFG*zd3=tTPAUb8VypS~`MaeoKogo% zl-jO4K%9YuK*vmVwJHjs!6+z&q&rAJETrH3Mu7H}A+{N`s^v12Rsk!UZdviQ`OeRD z<$$iYy0{4RsaQv*D%DaT@-b?wnNGK7?C$Q?1kv6~gB?f5&8;>ws4}O>MHSavjiPvE zzRZa4kQzZZSE0n7_F-2@WkeEzJPdhFX}QD! zx2_Rv6d|^bmrIdZooZ7qcPM?EfqB`nTJ%q;#RKyK5mD*6xlX_lFL_3}WwW z*ALD+RFryAPM40zsuTZXD5X%$n^&|xx#NTxYSf3bb*J*@*{&PS4j(stxpoYb;o9ox zy>U$aDeoMFtT1G>mro*+=n)8Qx9f!8vIe5ySPMP^vHaousR|U&f#`iYc^u(neeZ#% zlOUaH)SNMVO``V`Y<&LwIY(EoE`+=Wp+mr!@}cM7%rHhnN%Tg7ja0IUPoF-0Y-6L4 zEHxNuI~NlscNe;aGbqX;rW#-Eag0KS5|l7SXr}Z#GPz8K1hpvJ(iD^0KKqaiK&^!T zkD{SXu&ggEM32H^ga@t95737~(-c^zZZnwx^-V5S&O?HXj!vS>ItFEC8b(b5mIfiJ zwmmw_V^5`v)|abyHbWig2xzHFc9v=&b+eA%PIcT}?Qn5%NueSo)vS7Xxm7Umm%{NN zp&bf`*RK1CyKf$RIY+D1$PR7ua|bgVKIrUC>V?n}9^Eono9zh>(iutbjEEjW7e zf}q<5RHi&^62KC}B6?+b?^q8K0!(ouCJ<5TEn9~U9m1s;9v&t-<<=a(f5p(ohJ6qT! zgh{^@DW4i}CxmmQQ6N_S!A*7=6WQ|3!XB~-Fh^Dpx8khC`EZ{xSOaQFa88{Co(hRh z`|_Wn;;(cvoJ0@R5BF-rXjy5YR?WF?u}>eMOOw?auaD5XYeyG+RlO|SD2PT8S}<#9 zX)|=e9*ZHAR`qguykXyd@k z5&S)%zrkqA*=X2%P~%u0c1o2y*jGC^Zubm)(lID4DS@IXB<_NNofuYDR@TaHHdYIA zaZuH6(U=H{y&$$kg(u#vY#g~9wzgcomS_6V2Z1Sus-51%7L8W5$Q*nNkn1IC;N`mC z8iu{e=WyH6qx+U6{nUwfkahV=VIxF3);}=X!92ji#yq%rEE_xx{r&xr!Qa8&6>5R$ z1$msp!7>^8bwB|LxZ}gavT$qA?zmT5W$n<~9!f9;Fo!Yf@n2>pB^~Os8i009QQbZd zg!^34J9j!^cLb>Rv5iQ!_@eUQ%blgK5O8hXSLRu3P+Q?;M6L_*w;d4pl>7ZT(h5&(BS-27~_|m+u!U7#f1eR)CNZd9R4E#%muL6;S;I#xKzq8_M z%tWMTd+7PIOpIqE!5BQJmNVKZtSj#l|G<|p!~uQw=C#A;PeOO~g&y01a7;Nve91e9 z^9NpDZ28akLA7C?-==<0SzM%B4H4IsJq-}0?9m{V;ZleacS-{c;cnF(o<>j3@ zK-$1=&dzF+qcZKs-!iQo4FdNj&(7ZptD@JiI^8i)U=mkQ2t;XgbTmr1E<1L2^wxXp$d(!@+v+V=>*U<#*h$3 zt5NiW93(g}HN~bhl7W?v|K?5iGbW+{V}^!?|kSf)&psD$?Z6jl2V|Dcb z@YBhhG@PEC6lI~pf-HJNB>$r~5l8c+hn58zm&KG^6u12j{I|P{apFAiP*hX&Dh0gL zEz%PcKt4m8SS>Bo2r7dWATMa^%lG#<`Un{wQ~~#0@$AFXV`qU+NXR@ywlI9{=fX0m ze5f`Ph@}SZ?Wx-WGW zC=V5Mf1k!6*~dpCgCc|*0eDK?uB)%-e)N@BS9bysYcc~HTOp8saso90h_h$UR#XFO z3?xd7j|UVfZ+%3Th;K4eNJywjmDP6bsv&=hOsnjNhXZ9?75CYQkn-9b@dKR6BP{jxQ`wD zS9~rnw(}d7Cv(Ru#vn;TSEQ;+a_(FrH{)HP_k4VOiay7Os}V1Kt-SX=&7rc_H)7&j zvHP(rLt8QdJsj7%IXVWxv4L4+*u(5;Z6R-GZ-4g88B^2yJ>OF2E{a^FmM^l~WN>9v z64`v1(Slh773QsBPkXhH=o=xgbFmI}p-kYiPL_%}+}6k*D?`E;7Z(Su8x_hkkp^yl zsKI#RVrU7)cL6+W`Gd~T*e81Xc5jhI8pQ5aam3+1{OVi?G|;YPI+Qn5rILc(I8i(3 z0EPWjv^Er4eBa`~)0HTWw8jq@8tqVbq@7xy=^`W5g}zj%5n_zSR%*iW10@54=@&n8 zNzTPj8;iq%V)keWrPAE-PpP;YZ#A%bdQahHs+F=k?(M)Z)k+@w8lkpU8-~f366+;P6KEAL&#i(?MJ_k*?1?>)+GHOiurk*d7CT>PHXg9`zEF;4?9-=D?pg7n5iKUr*5uUrYhM9cMUQm7HB=^d24rLfyjfi)4e+7P45tMS6N2~wXL@0`29V)XC@&&jE&rI%>`RTt{y zJ3#Q|9Gjgbg5cZ}>TPY66N|^e!2$jrHd)XaKFH3j!0)hIOg*-?M#sPa)T+Ltgqf^UF z;gT5$zJC96YlgC7iHq3vz*D8x%i2wXI_J-yM{W+F*Hu+Tzr4ETJ^0JGBdN%cWxNM; z767UrvAH>EQ>{lX7gbX#fZI6-IoNBrt9D~!1Ax58XmT<#$&p)*8T$D65l*{}N27|a zt4n6qxMY9V(1RB35RX0N&-D05WXOsen*&#JJz%q;qoYe(!4ld4@Ut$XqQ3ZB!Ey@3 z)kMnx=C-_iA8Z(LhEAGGV9J0V;i;=Ds#|7c3~-c{V?M}j-z@*VpHMH4K;)dJY8@Eh zK_ZjI!1nu&}T&F{S#~TGPL^WiojD4*h2&)YA^4D9FK##3~(AXrfF3S`aap8*NcIGOy~r~VtnL%rY+`t_%H1>XFB9`rjDbpETR z`~;nU;J29RCzpRlW`CGyv#h@%UX1DX>5~^2@08gUzCG7F)ITcy;Fl@-i=_E`o#67m zdN=U>LB4)%ngd(|xvBNz$7mQB<$GJxU0q$VUuFd^IiDk4r}H8G_(zE7lvfymy^3Q# zQsoO`Z2_=x;KGJ;H86np z0Rw_;r}z(iRhKLs$H~PtseSY2&4x%WGgC68{9ay1I3y<-QoNcefpB(8Ar<#s^4mB27Krg9T3T8WT(&{XF>+K>RQKK*59~peG+%+n53DU$#Nnjn-P!3WH%Z5^c~N%p#2!}pTipBpbf_y$Z5l#w;6$6eh+{| z7Tg6Q4@C>d@S~^3SO_L?qk1hcBFM(~<{2nL`gwMWh&nVt0CWbF&##(7F;gWbNljC8 z2pS76PR@X;)M!kx5FoR6niFn?QXrAPMbV$q(|VEG7q=wIs6@M)$ClwvY=sTS^<}cG zf%OB;ESOe#ZK%S_JO>2TOm6pEV1`kAtts?6-5Q^fmR7u#lAJtc03DB{Og;`%%x$8t zm_=v+;I*Do3~z5Sr{{_Ptgxk-ic5n4A#VjzTMT_Vro?T#_VDrJ0{7@IU%o)8FE<^< z`Uenr?%^T57Pmt>2h9)2;7a*M8CUMCUd)gi0OHVi^B3^y73+hlmk{Crc`=S2edg+V+0nW7_coCtwqzCW-?xmU~No@v?A-SS4Y^uH!B(o=53uJ~%v zos;{)pw|iu3Vv0UdP#UtP=4iee>C>ueXF468-ml}W(>rG02cZek)yxNimctzAw+cc zuU$pibRdh+k;8|lT2owb^>wyt1gKH9>^IWqM|n@222i&(;x9Qg@7zsbW?kjkMfAjRoke0TJJT;1R3iGM>Vzm_*>nNXVQ!mma4r~LbWqi+9sn*a2IJI@*Z zNsjz4itGQ1&ryT}qjrNIT)VqVz&U{;G&Q?Qxp3Wk+;khG-$TEUm&e8id&ld=i?;SX zq5pv&qA=@#D*AYFs3P|fkRoU5A3uJqn=d`tn&N*a!)|lwfr?60 z*_LB8;OWfF%+~5OFe%c#<{TvYmTBQ^eeWTVp_hv|btSZy`)L|x(O! zOg~tte?i^Nmb;MF+TPDptHZ^6y=Dy_qmb76-`33{t6mVYEde7m-VhbVpd|*P7B(?w zvTBMGOGBYNeHWnCg^f{b?)x&P{E`}LFc;2 z{4jZD0SIIuH`y&`&eOtCKMzY{LP8bz0dPiyJPs;d4N#(ca`j|UZCeC~t&Ppr)|SlY z>8_llH&K*Pc1z$XlQSAC;HJSc1rF#;4U2Jqe4OLn=6LPal==7yaZ^8I5+mdwAV0js z8EvEAKbb@4vH^PgJc|(_xqd<F>UQv+hc{kLG&W#1w!J#+kkib{k7%5D@(mUv7|*4MXa@ ze*HQq|G+Vy*^W7HxiljBI&`d*4~P@hh(Pu#Am!45H~|OC{q_z)sa>w>rU)6R{H-od zHrU`lVYS=1#$^N6Rm_toBfxMaPeDlu?mTaO-HgNjDrMPfSW;(QAI{cSFncHG{{C7YS~vg?gmFk6ISMO+roBnSdfczZ3E@{so1O z`wm4^L!=cKZG_!z#3JP+7yW{wY^JCd*NC)Yz{tFq!=k6Bm+Sm-@bt9oxuvD0GlMVJ zZh$30Ix6nCwljq@gN{&2I_xDtjlhA$JC4_ee9X%7h?bc59SivI;X^Bns=b)ySZxT< z3Xdh`l;7jl{6r{Kgvf5hkMsf~wX0*V+J}>5AH3YLT?B1lB)5H0q8?;+Zf@>SFRDx#23%<4cT+9uAdOH7Zbyxt*oq+buWV64D6iav$MO<)k%#4 zGlW!VlV4n26~B4&^!DAi4?|4dcLb%z(vej{-yoTK$>Zxh(*~r;Ew%ysR(u^9${lc# zQsD%jZd0slG91DZcLT5{m(0tQ>z&*xB4sj$O9h}rNQ3}bCS93RgHztDv&G@Pwt|1- zwUXmlQU(8l7rR%7A{ZABNw_}lp#|903#VkMV{GtS^)vuyE5OM;AP>MIaCel$KiAAN zdgw;xg{j?_;5Qs2;0wuVwu_U|Xm5~2YYT7ws!}bkg>62MB$4@mOL8r7&^a`qtqQ0I z$Smq_AKu0BVJdD5-~5R~nwocSDXaNXDq`*qa)#^*m(^T&5fuf6@UEdU)2}oa>g{h- zAJ}RBCgI0Yr-6U%etlmAzOf@BA|gEeDycZou(qpH7JECJ;7h19^$1i&XJ!Bd4d6b2 zUR`aopUlJ4bE-WfY~w0jMGOQEv@B#I_Wy5(^6c+>f6DcXFpwn$h$@#wG{H(03s3`EA z?Si6BreKNJSrsGTHbJ3v%|x~Ust)+X0cHqrV4pe#-k|ag3qoiT`*J9qAYgP<7P?Rw2ud@rxh_J|w@^$GR^)T97sY%}>&Ksh?>lJje)Bwv0eZ6;xc(TS!%Qv@wgWl@dTdg#B%1U%yoGH0>O$Mim&D1PK$$r#W4X&gRs z#gbr=$iLZxKhVYh`ij5nP@`Qa3Fh_d*8~Iv-OoNaVNw@z()$cm{FzTczd}KAiiEoa zNJU`uTHoTG5tJ%8`+H?rhPtPet??edvIZ!{^Mr&CRrYH6&|EowZ>z77Exy$O4go5c zerqz{@n>TVpwt)K`2D75rTf=PK;)S#-*?zo5qimEcA9Un{y(`mC=_H2Iwnn#r>g{) ze4qRUw*Q41YF&jR9{0h)?c(elaEH}=q7gs~DTvEgC^xI#{WT?j=mBMm_8_aGJWnH3 zG81PdIiKKS`T3mqJ3#(G+}U@~7<(Q)dbB-77K5s_ekIsV?mt&Gvq-Iksyj{uTVS6K zd9%K4^zyM^I+Q>3ZA&v->FMd6ot@90pD;Yq$r~9gviNr-{*3AVCfELdfcyWi>$tx) zO2cX(2|gNpT}iyWxM=Vxkb=nBi%wip~5>>igR#W0xjg8_JXhbTXh;Toxc_LIMKJx2Xv&&}Lgx zT7X*1HR#O*T@%i?x3^!AH0yQl7ti~l@22IN8 z^}yXdEmRC`2S1>vCaprPoR|P)&`hFe&mtMY-j*au!o$M@$KnnhIr>0b+tVh=>d6TL z20@~3Jtp=9I9bGK2%LdG1&8I#>D5fjuyL|<9%Y7mAD#m3;-Q708T=;%?EbVc__6JY z*x8p;9a)ZBD|$+5ZytzdDCG&-g9SCxdMpI=O4;gIEk&Hx$+w~F>(F;eOkV#dR1cFy z#HHV=0aglnM2<&a5wyTo%)Ld6P;6=Vx*OmJ6{VNR$2OzoWz6XT^cR52F$7hQXq-IAlfR3$ z1s%uj&=TBkUX9|g@+PLKwuNsXJ9j)5b?P9KG})miyyv4loDgZ_AUtIPksStZd1xih zhM~TTrMdHDkl%7vmBTOjs^^o?y|dPV4xPCy$2q*p7G{+tC@=hL6<6{yD6L73nt`2_ad z0**O!U79F!{mPw$_;|qLt{)=b>_IT72uba}Wy^OP7Jd4*<@BKsnM=%iU4lB>9wkvZqQr`iaLnP4H9kUKim zY+rO4M*2MRo_lDv1W36w6C>*m9T<{4Ugkc=691S!%rPo>u?eO57`2y4wuonc?k;c##CC?*oec< zDx5L9Dup%+u$Vm?oB`4=HaKU2JyQTD2MDlrV+X`(QbgktlrB)_>D9|>ANCyRqB8cD zmdu7UL&;+WQ^j2X{ot)4cpSNJ+*p3C65HRV>h9wM-^7Nus>uWzBUqSQ;;us*3si!q z#*BXcru9=MoiP9HGURd+U*EJ%_ObtP&Gb&Avye`JX&fm9-FY&wlMs(IQa^)RDqka* zYZHL=;6`5r&fz--KAtdDheEPCqqDQK!^39@2&lN-YaZT8F$U8!oZgDxa(BmETvV(x zX|uY)#Z?M43nZg}E8DBnlykSdr;B;N1X<>R_NkS}(*M{+lg+gO&TCThgin7T!z~|P zFySv-{b3Nll?;C!`Ul_WI_mN(XhvQ>KE`dSfp;voLIywJ;o((~kTzsuaQOeMEB#b} z+ZaA!2->bpNCW<@+` zc3h4lM~(zyVp(4L%kuX_ZG-~%57j^lH6^rnvZG-afa3vlo`gqdPM-$-#?y)~LmcG~ z=KT-a&4L~UI4?4%!*iic0e<*q4ESb$7*Kya_aA_)3_k!^*&|25%IV9WAvUmz!1|By zd!=^s_H9CLJJvnAWcll=*uUtfs1f~bokY1wmS$Xkf>f_mB2}?1DSlQ0f2#5{2*mjp zzwy%ugz)e`iw~%8O}{M+0_@`l%m1r`^M|SB|4Y_4l~w-l_$VXL&qG3%H=m9q(s>;q8zxTB*Zs z(c9Foy{vSIJI4;V{lCTHHip(HBSRp0KY5%51aKa{yafm6HJE-<37PZ*Kq!OSaUfi# zj6q$1uaP`)>Qp8e?}4B9CgI6^$o-f7Y+ikT3!lpzNT2-td=^8v+nv|mX@)4r*lKPW z9GxSw2e*2?1Y^78sR$u$jLZSBNlD;s>oyZGPMHYmm;`X6jytlF~C z13+|CT)_9kAvanI3JL~>CE%qr%{;wO^5N*n$OzyoBGyMvL5}cssvvnf^Z-SfXFL#j zMh}j8e>Zx?ab#}EQBC|G?*2Ov>%I*i$FEAIBrX}DBpS9d5}Dmd8Wc)q(GW>S*(0UQ zBqAy+B9$W9qk$A=f-ut->-3w<2=se1ifam z@R^F@@l;qbmvoK%=VFPGgT6i1mbLx1;-m8`Z^^W6PvM{vY&Wm?jL1L&u@@?u>o-LZ z94|SCf%v$%V|sdZsQ4+-J|Y@q-mj8%(mtSZib;VtvSK9V>Lh)OvApWNK8YksBpd+S zrrXhe@gQGl2_rxN3SZENW6<^uY@IK49P`l%vk&y@Og>m@&>@ZbVLH6t7yT-DdVqL+ zPdDvS^%*#M>?Q<94hOdt?KA0`u-UWA4S0qx>)nAPX?nTexpRk+ zyI{S(_`B=;v)EGQ?^7X4+uNus{@v0VP2FfQ^cq2*@priCZuh@@&1_aEu7(Sj31D2X z|AKZ!M@PpiJB1I{Wmp5G#i_q=v>)3_pia9@h(am?X0&-9O+`>|2H^aTEXtCpOqQC~ zdf)G)VzZ(23iz$Gq{K-eB;NKkOJ|8z)x0k%uK&s7Xi8w|Tc%*!`C$*BbOcr!sxjAD zgbh|moYcE1bvipc8{JQ+P`mcG?0VgT81R-10{)*vLyF4E-gKWuYWH4SF^N~*k>%pr z>XfjLcoJa5II6W(RaK>>C{MbepNovB_5ZvY;Ow4KQ&v_!apJ_jeZi>;3xB87{^3^% zR!!}hZ*{C}Y&8!I<(e((H+%d>)tzY@NFpD=Hd9wumzS4EunUIy^#}>2IXln)VTd^w zsYam(pzp_z2L~Rl6Z|rKo5#)TPfq8Ts2KGqV3KwkjQ1!I!pv%;sT3OMWj&Vl(lBeM z)>{F|9o0fzM0Vr{NUzl>Uo3SyKDG*ig9b{cyIu9D{DS34;`=!lWn8`yNxzlUoS?qv z>2fNECp7P9+>N4Q8UK-y)@cvmh`9CGr1^IAL&;Kw&ZLXpiqd*J`xQS&<+-bRcX64ra|~UFN({^fOFErc`1>sfchaT%Ij;gz7pFTRxlrA{og7M38s2Q_7C@vAO&I{WzIP*oIE2=$;`rA8(D6aq zc7x5O8Y1p&6VDk)VynAiT3tpy*WX=n{H|N^;V-4^7TJ~{^hrueLhtrr9gS*x~ToH>|~c-`utl?{W|+bX|SX+$@Pl(g=s z@mH#kPn?%7o2*1)N#fu1tcrD^J! zr#K?tsnW5i($U>- zIYkpo(>eza2XcR4_`Wz{EOUS|7Ph@H>ivq7Q8tazM4uK~L@VBQ3@Fv`aaLe)cx0qg z!k!BT3Ef!8!>W*RcDdWW|NLxK^ia7iv5|G2LwdjQBKJOES#I=}BXda|MoYKdqb$K^ z^h>=fSaC_4XfxWHk8Y}XAITL-(f_XctJqf zeuMrJoIrkS{{da&ZC5L}*9CEK=)7)ZR%LlJ59e4~nRWUpz08@sy}g+Qv=8R40DGQ^ zHVnQgJbWC|BJZlI*!5Q{%AV7pV$yw6T8cKGtB-G42BWTs4pXzQ9s&_3VHZ$`0+D6h z#qXN8dHZU~*oznUS*Lqcc=VH|4lTAR?IT96t2EepV`yO9+MEE@v8~ z)-f;$zk2nEa-GUUC#fiK>VeiQJ>eDgA(-Qe#$PvX(f!3-RX}~rKRr1MZX?WZLVWy& z8iMjJa@fJa0Y8DVPqy)krR78ZuA|%=HfK^`BwaR&54}hZa!L&T22=e(EXrE;JTKm8~j647xsLXGfUrRo*EguH>v0q0= zM_1Re=aWJ&a{!;;nSc!zSzVg~9KnwPf;Ng?ho7a})RVT61(Z4wcW7teK%HI7eD}!F zqihi-Kw=O{pcLA(rMs9x;oP`b=Y9oe*$9=Gvw|BLb=spA&Dj36+&mKW^UAy@FH@A3 z!g--y4Uu5FRk8vudR%9|)#vC6bk;mBHeop=EGz4Rr7J8x7H*H~>NcwKC$+*|(S_2KAd+di|l(GE=L6P5@U5AEh6EY1{7R1wzZV=kzZ8v9!yb&t{iVyMtL{d~ML@ zIGIayMqp!a4mPJLssB`aZt>H`tv*TyjO2GivIzx%Ax`Fv4MJ;0 zV(r>yNONqc_3`n!nd`}45-yX4D^{4?%-06R5x+s)miY$r#dDq5RaPW?C_qyRa0dIK zoRpQbyh9y>|7K0m$Z&bS!Qr|l(;o=mVj zoQgMc^@23oK#TYg#H#_0AN{IT5#iyoXfh3G+xNggkl`bXq0^yzaxe?o+0so(DL@X5 zGEl?kk{X#eE@Xn1^pok07Y;qYVBV1X(Qg02FSQ^QVBOF)qGSTW{)K+63!n=v+nc`( zG3GMIZV*^{{i>h*Gge*dQi+OUQ^IY}J4RJp4D9+wkD->AijecOI!cE|vQ1UT4gI{Z z$Z5ckKytSSUG9?>@$9p&4aHQg_56%BvTv=MP`+uJz*q!r7#xnq!6RAi;EVG)$E zx&5EsZwC|D5#kU(Lz7NI0-sq#tKw?+SB0?yZWYh}xhq+>wf&hrY0CW*-ZE$L1v@7}Gds*1d8>F`@Ms`)}O|9%A_pbu$-_T#5V7_>dGUf|>Mh>MG(uLf+r$VmCX z(L3vpPt4i6;O}lVdhyv1nLu`;C1SMQzG8gd`6aX6({z{xGnhTnaTf(O<-YU#V#XFy zv(7M<@V0YEB4u3zv8kzN;8xH@__Cx#?Ai8x`}P?aaO_u@6R(mcxuTC?sKPp~>n0 z3{8}Ng&HodaEL9X5fSnGqpl>o#jW<=x1jAoKjrfSY0OuMkT9CG{)g zpkMJ@9N*@(%3SfsHecoF;g|gtO3hf7%~}d;3a2iQpiT;pfB;}4v6^|@ZLq%R+PfMQ z6m;W;Gz#nRj;vka6W;_=3W*VVPZSiM0&?}GWg`z@j0t7{z*ScPAocxDUbZ`S;i> z?Yit|RBg-@S8oi{Pot($tn{?n5=6CUIgq-e3RvA89mCm~|Gta4R3OXXVH z$1}A=H_0zA%FfP6wt>zPpmTOPm+TLn!}_Z?-B6_)Vp{bp5<~chmV)&%_jeoKNZ@2> zdaEX9o-4q^a}DlFj?>-yAg_xmOug7XXuJvM7t1&fdNLF z<}V1maz!yn2I6zEG#eW=9iBWGni*nGdP$Z*72kMKx@KLZl&1*O_lUS#&D%&qM>t^*iF=rwkdRPUSGPXg?pgYH z=lP1~9{-wJHzY4Fcz(r1L>||aH}Nx1gT2nGSP-}$jl-UxS${FpIHzbmIl{n8Fu-HV z5QM{dK6Uxuzuqjm*Tm`^p2pqX{fSTXYzr~t_A&!=%lA<5R;BK9u3MUW`ut6utSezh`4pN5$?!fSd=mZ$SIul%4k7&a(9MhI-6-~YU(7rDnuqq^bb4y7yJ1Cu|)9Thb-F%LusdA zhs?HZRZ?SWxYK{ej!}B6TtM&os3&f9}hbFwnyFl&%Bg!YOML3(Z&p{DHk8@UAH#)^_NZP z2qDMgxa~%tEaC}mZ95Q5%MJGKwW>8w^NQ3mG|Yok&x5>i9Wn2oebYJBQ`{Ul82u=7 z=`wV$AWh_hXeT-mj~&Z{IEHeTrw^n!3CWvci=6NK01^6~iF2OM(ES>LOMMnJhsg8& zPAfVB&!bK%G~K;wecLg*5l|tktzL^)d2+Mx-+-b~W~Pn+m3h$l|M5yRIt~H7`JY&N zU)m88JghIxfiW5#0=%y5sIRZ@*h$mE7Vfbn$r*p;s0s#XTdY`1;>ylDIFFuJc&SCnA-Rn9T5i>|Hc0LSk$;Wp|gAe(O5`3zma~ z9W-~n6&XVUg(C{=N9b+r;a%)b{C9UI*Z?HE!o9_tsU-`gLxY8aXtvV?thhknlF(0`p zhvniJc;s1+LWB47Wqz(h&sSlycZW-}rj*z5ZY`XFT-mHM3is4TbK1%c2cIAYc>M*b zSQ3P5;J8+UE-5493=oSe)$0toB;4+^7m#7ELDVN%OW#%hn`k|8M56V&)AkNK-M}*% zVOR~qRf6>~-Yo2DUygMoi~^Jb?or?6asqLtz$q+ot-F--jmQX@wqw;0YuNk=QaP|b zEX>So4H>jGp2aU_JZ9vg9q-OdZ1^L7oeY_OEK|72I}75oN1MeAcYi#nUXAMXj1265 zhvhSzZp{6;T!NC7_ZCux^2mt(2RfPZAvhdV!VgRRi({tj@n3FGLF#~_`pbedc6#fk zam;_Q#^`g@-bIII0F0huTs&MC;W!Ar!}FeRSvV^^BRyBlaLH~e-0oafgzv@2oa9^( z$ut?JQXV-qcD=i*Q0nt0k^kXSgmgw~YIG1PX=oUCQ9~u^i;$G`;oJY=QuI^}_4NtO zfY#R5{aRWFem*--%_kYsxLR)db$)|oj|w}Rn|GN#$f*Pq3JCzjl8Tzuz)5x(Y>cA+ zk3VA89_i@lz;v2OzzWFNA-Y*oToMAn z6;NvrRru37_=D8Vo+P>tyLx`n%v-C7=LNW9ZEcMfI$v$OV!<><%+zY+nHqT*kHcwb+==Q}P(SP5c*Rccm2()7!Rs_9qO{<9X zm$(@(HY4{Uu=6aFb~7_GXIaBtyZj!{V@q%Axg7c*esVU<{{Pc4_nQ^d5>fE~3BsQW z8cFIaXDs4m1z;MABFXE|SU8ef=HJ)j&+nccfd?=rR{WR-(|ve_g$W9IlHT6EpCRSf zxy{$Z%r5`@e=G^j{&v*42{_z1BR<{)=?;s{LJdR1h=_HHAi=;mBZ_ct)bWfx zMeBcUx=iu=`W@mOgL3X%!%ZUY!Egshj(BmJrh<@o8f{W@?s67B6OlGjnmTW@lLGk| zI8a0{*RNkU6PQ5S>N9k)2SBb*Ggu;zPgM88_8K8odV>t73oVca2K8IIvyENIwrHtD z7zIilmjTGgdMRiZa{8R_)!yC?5*DkpT@Mk(2?A7i0b!^xxe)^d*)|#}9;7kiZ|hO_ zm|m3Gh!s&aVNMxWJpTlOux!<7XfwtC_ zALgF-onaT-@Ej*l32+^4TGgH2bO{dwjpG);{8%N<3MMb104N|?>}jtX$Yhcnsp~)HW&J1*$l(R+SOe7g0TYTiJur0 z0F)|T6U$QFu|vtHSHBKb4s=T8+eMaxSeE?g(d+l4v>|H4ZzHK${+>x$^N!KMw}Tha zv8M zY3|>z`}2)YS5NxR>4Mg8F4eE%>j{XTP1Z(%BRtSfHDuKKH-G)3{P!ROahCYnwXu?QN*5 z(+k*rpIdn)@#H((lVT6U{}ROp7ShNNz!zd9SPS?FB`_&~?`vJm;&o=6pgEuHkB*;c zIEbK0h^|PBW~O1x-u0z4B_#zAriI>M%E1}_!tJ7#me!GD#~j;}Usr_c+J^ zy+pm&$6!$N--Y^1rZ~IaIStYkw(mH8yxl^41F|NwWdrh*lIiNB7F;|=ga{;2F#Tyx zNI%5;Pri)Ng*s&Y%o(-x1KTE;^afpfIV zm;sAkxERf&cY>EdeesWvz-RF<`aHe?|2!FhmEs?#>NNDV(?KM1aZU8Vsi!^aj2=FM zuu}ee5Xr2QVO5`Rj)L_;J09<66rJtEFJjiuONbOiC3p#)(w_7W_;9z*YXgVnnNpP6 z8{HeDcowth5+TUS!0;vU?x|RZ4bqHCjYd1BR`c0!A7JGZiO#TUDjCd2>(4_|Fo~r2 z1mH)>wRUC&rA=9sXLl>^$!ZvT;~UE29|?HNzCNq)(n@~WCiJckZz29fUK^zr11l;f z$*zaCXj9zjR0uAnwwyHa!%uyqsQ0aPH$YKZ-bGqmVS%QCE)bsO9Ny8V${Bm99iG!l zc&4HB@}g!nKUQbBx6^Lst1B%dzw6k`-GR+!%%#E zl-ng8N9}+zY0^^|YqG+e553p-&VJ(e6bVOvb_pSnW5di6U%q-Z@>0zu*}8SNv3Q7( zF->$Vz*j6o$?Mnpkn+eFdng+2`7g zK`)Cfu6y>l%HU^z#Kb5{ku1erOHJ_^iVn*3594aCBN|`V+qN(&Of*dVbcIlV zXGN@!HnUtarc8){7LxfjahaU=MGglQF%U6JP*kC32oK|&dG1WSA#UwIG33wB=6P^k z4c^B*dld3ddYL7HXlt$ha1a@MK`}OLVdom@twj;U%P?$&K3pK-QMAVkgk4te+YIr= z_q1=qWp?1oL5AEP?*&0n1p#EYbtaIAeh^RwHd<@Nk&}n8$;yzwujeE+%As20f?=2M<2aB{6-5tD8N1oYw^ zp*7;!j2puC!(HlhsICc~3VKfvDQ`F-eucc_s+3j zfl;g^60MT3yLU4xvZ{*00itMRMY)eNHP!j&naPubiWV^pgwMg+%8FS?&*bEUi2l*> zfrX>`>H*Snqdota*~V8U>YLSTs8Y>8_*7%rted}!>>n8-Y~^}jpszm;oyHlZrJ|@0 zYA<(Xd8fXA|FNzGkrLr6g=g(#g@pTswGeCE+4K7`SET|@+L_D)Ir8^Rw5~w~HI}UT z+rTJRXUXF|GSlMhic(a!`0Ecsm=!Q_%sy!lrXoXG1TUWO_~m<)_wPs0lHUFC{P^`Q zGs>jO#v`Ykfy)I6khIjAppX!P$fhmC%iBBb#4 z_UtKlpHGvI8Q^h=)VE=CqHWRd0=y(SyDB<2R~FqU&~z^>2ID#=Huff_gFbsq;$&CwwIO z=u^8K(pUWU8sXlN7#8>%&|&E1%a^ZT|5?ax;dxFDX3sK5bF>vYD1Ym)-$V62rV@yhZTmMWXUJz z>5@3bb_a4qd-0k0R6L*m{GUCHLSdazdtXwsy+h=)_R=6eyRaCkytUvR>)DCIK5 zsn_(4Xz-0pG+?O(?;@J!kzU|6@Gp|dF@9h-6;r6j;T`?WV5<00>tK_?vbi(|g2Khc z#l&Y2@LMO_B*$S)k22+*#7_Cc(km>CAY_R|w{m&HT^bPzK%K7#%U5KLu9XXl&`@4&Y84S_S*Nr{5zQq%l> z&&^AWgZAIoVe*Qb<3&jY%*@Wsh=EV*(2|y*#4a200LA8-LvOEbrwdS?eRA6GP-mi@ z6dN~;lPfW8Iu0;AovJkBKonzhoSP3 zVqQB4!8M6o4|W3>hsL#gO^`cX2@C{Kk76r%AUghS%te#v88Oy6?4~BH3zHXP4UX%V z1>lAa`b7U0iQ%_2Cu>Oli*`nB1G-PYkdf1lsxnUs_tg zCa#fMgy4zi83*50-$>G@jdGZw37|a@bTNu^*qLetGhDg3RNI5pKrV7FVQ5fI$?*aH@3fYn|&{@5FxYiU%!X|O(<4*MxaNr z#1z}@Hrg3Q{4xG{Qai@uTZ#Y4g_ZJ(B>tD1=fmk&D)aslXfR#DfBZY<5pwg3mi!ll zkkY`S$j%${%|1|6#RbnEUxS_f1F3lLni_nPsxu z0W{~NP2!tR*TeP@={0+CgY%D@r)3uY#d0V69A)BUnhwqhCqnQd-W;!}#f%3?$?Afb zIAyq5ewQ(MTC)ws@9l=iem$2dxj#$SA1ixvu^V6O*()u7^riyS3a}rxOff z`QT>d&+HB~01U)o_vtLkOb{M0q$=|HFRRXOWlYq7QjCVdSV7~!1Vn%R($GDcu3^ogVC`@J!WNtc?xm0@LOxr^}h z#W>ho4^rY3^()0%>SW=GfjBwVE>|?W`cY52k0KqS-hf6f%uI?v2gw#Gf~O$;AQ(oLw4cpg*Q#5~gzIRo^Y;fl1r zg3Sc(@xQ@d?8d-)kQ(RZg*b*P8kH0kQ8#v& z0}csy?%=EyPUKlnX4jK8okDdwUJ}DnPtDhN$YejSK39APo5p-QhS;iofFaOWBRA16 zx1WOH7!&FDuiN1lI`C|{0t%`lfL5RuqPv9+qZzuR2Bhu+gTuTKFhz*TV|~TKwZh&a zMDjgtPPR<@j@Mp{pe~2sxN##atOgU&ik0?IKm(ZRwFJlQSC%j8!3MQu3j@X@)6W2B zL!oY$^mBlr?d{`J;0mPP(D}R8*C1M^%j<~mBK!>^WSdb?ZZ=*yh80EPM3^9W4ah*3 zx|s2hwoKdIr>w(|($j}@^FUNoX{HAtX0FafRIAJJd1xQ2A4SqWqb(lWQ`2}H2dNz(* z=(&j68rYOdl_=3BU!PKa%5ZX&gnE&$%MMKaJu0e39EP+Iariouio#N29ULUCA zZu4aoEfGp;0aX1ugqkmf@5#KG-VR5GLT1FrejY-6L#HN3(`{MhHdq^|A z8upU(DH>0!&t-DpKxp;VkdPNzaP9PqCYRWPkC%5*NhqE&C@2M?9#~WR4eVnHmalUZ zwxaO}#XR=h2A3CTDVU!aI8tWe6N&2R2^5VLhX6_JP*N(G-%W)xxQudE-TF*_)qkF! z^&}=MC8r{CIDefIf>CaT-ntxINenIILXR>|XicjxHQU?}z9+T6y)31%pfj-k?7W5R z)WRXvd-ckd3PgfCUPYjDOQloGZQabjKwQF^^<=vRI@Qqx&+<|f6jI>*AoPcX$&uMy zzRyZnD1p(kz;OF&(cx@-!xgz(UWPLNB7Cqxl#N-p^V;QrfI}D5{(;mPN`&iL{z*Df z;w|u~#A;120d_=o2|OC@rX5wM_we~Q7`^Yg9lZZ$Q)wkN6*SMnmEC@P^uT#+38(I()~Dl0Fb{8vi03gJVP zAmL9s_o6sQSd$MF+Y|&!+1LJI#KQMhVkb9ZC#Nf~)7I_JXl%p(ZR|MP`b}b-(_z#l zBFq0Ho}|=#|N6}o^tJC1B?iNja&o~=HBduM#UE{qy1JAfA2Oo5%BEC?4!!v_%-4h` zOJujZvnGE?dAG8%lborzzUoRQfgI0#W{1E;YWmDLX#K7*B!eLs>3sF7I8yBi55s&^ zMEe1EVcdW^Iwv6!g0oIjzl5}r=~>Lhhqq=YBk$I_kC^XQ5G0}{YcLCq?0+>dY^b7g z`>@!&;vc&`|AN7jW^=;dGm>Z=$jT2xs3UGTSN?>@ruNm@e?Sit+Z1O-DX&9F^t!m} z)WW}E9&5QxOeE-8STMD+EIC=^5x=mo|J_K^7jjL^#h%|1D5ORgfQ7Bua7>5GWbfVz zfI440It)O=K)dF0(DAY8q^(CO3;cg|&q*XbDdc5GsqWvu|9#jAS!5-es@$PeSX4tv z7Sfjts<%I;9)bLVJ&IjmD8W@%${w3Ugs9=t@JuxpoQe1E--P}Z^ergN@Rqei!JW~q zt`pdT|DPK;w7i+f%>Zm~N9=$(e9&W&8+$9KZ#qszdigsRjVL*RP2qKMIAGeBw6K9M zBS_L-zag}fmv#5G4XZKrOeK6>d0&m$1FzmS!G3!RM2ZC-b)G$PZdKB6c`*=`u^$)i zMxs+M6Y)LtU<28|G&k#aXn2z37meNynpt0%%;>~=7JSAv-E&JBpa@D(g8ckGjEkQ* z+L^k~ME|FPf-@BEI4B59_EJg^`8Q-V9oD#qCEUGBWH#1<6x_n-4F_KivjQzDV94Tc z-!817+y+l(%H9y5D2e#P=|JSS;=?2|MCogrjs|A9Vj>;-%RMH%<8K9vKVqg?R)u2h z|N708=PN4Z;QPmavas=8=2u}u?*cHk1IvuhHPmc9(ni+&l7@DD{Hw9{_8U~U&mh}i z1XStW;~@Wh9s+6VWdr5Sx9-)^>0n^*it4lCjxVyh_&()CwTR^?#^5EnY#?1;qPFYu z%$>2zc%kh0K!7}Of&C=C?sd^)mmYH)q4cNu^ik6+)YRZxQh7bUF;xzx?-leR4K&r~ zy+`@Q*;NaV5XTcKKNu9MwB@dy!we-u%X$m?otbx;?lX@J!NaAF)MHK!xt zE`~$J?Ye&SOek4QSh#eF%2wobb2riNj#4T^#)3IqdIGvx>`y%-KR>dlO?~ZZPac3= zz9WVNZu4VXn|e7oc+ln?qD177#3&;I_QzvDdT+JtR`4McQQh?ZBoRCFm_93(*;M0r zi%kKl7`?Yvjj60D1%Iuupuj6F4;6al5LSfYe#eS$CY8PHP zpuxLhOBO0qF=dpWH~@rkNO7{B@_Ct;xwmqY)L~`i3q!+tAlDlmB9PmWUF@FUkvO@4 zFt2&fy8pF=Pa;S<5F6balTYxyqkmuqCUXw(RHDHH4NSEtc`1t3LmQxQ0Y-qWHt;sy zU@%hJ{V{#9DTmk4M(;Hdfq@zZ1}77%(In098R^FL+hzRqmOwE1$I;;S6y1e|>FyFJ zO5WmsWti_g+WVqF$YtbhgpS-*(H&G{NF|iKnOAG9=!fRinoLkNTI5!Zp2rg85<}Ia zVEhYp6~`GZLS>yD&ue6)GH3bU25H@n5W%hJ(Jmehl-?ygNM z{qPAoFaS7snd|MEXpPUI&*xd*SZy8#U1@%)MWXAyDmQ5zG7=0$ZMOFJ8>~k)1SNE$ zl8&-95z~?V&LbOR&nfqfT>V8YPtcvMw~r-n$U|Fpy0~LTCD&^SD!dXQQjXbKC$kGY zBY{rlK-0cE^Q(}kd+tER4kLw8ZQtUU{bG8V_wL@^p`uds>Xk$5P22nt%y{S*EtR%@ zvC`JF9viWo{lreHUE8;B%vAPWzS>|MbOI5B$@+kv?2iv7-F^mC`Y9#>+7l?vg`yW> z;VLIikq=C?UT@lHeIu2tp3})WJ^lH7P0DDy?PSSlp-*JXytAL#mKWpb&?vVsF6#Hb zKy7-W1N}6^quB`hz23u>DE^fBQKzA!BL&h4uztX_M7P(6usln$X8wNeCG5Jy1Y-oO z+*6O?{Ie^qU<6`AU6|)HKGrayA9II)1y>t@n0zZJvqMiyNr$Gc=*VJ8*Aa8F&Fngq z4ect~I@d$g16vf!z_UyQ7i_;6Jz4XF>Y9G&7YJxS{FFETH4pKtG`jPW!iE&Qn8L)4 zbGc9hnkpRme9`#C>xOh{KIy>^#t;){Tofw&neuqTQ;)~gxNXf7&<%a=iGyppYOHPB z{>_SIC}yA^7VcItrpU-@bZB#toaqEg^ z0*cvn#hnu4h5Tu5naNWn zFUfv`*JY@A1xYHYH+8$S!8;1%kk;h?&7?ZWO_siv{8r&@Z02C^$_ZgtfEYv8P9uMr z!R5ot+&YGK@y@KA^;i)XqS!C8j~gYim4EXB7H#AK6q;FbU^Kq4c|n4`-l2~0IBpv( zi9asmj(5bAO5Cpgx^q!EA(NOWb%5gr!roJy&uS~W3xfue_-@(a*+qR=lI1myNZlye z%x~_{;M>!(`W;k$;T$p!B@uEBHGHvP1$lplf@x$x*Ty|S1u!wj?5=3sIa^aYpKB~K z=jZWIDq@xU@`IuS!(YrGAHW6SksSEknJ07M8n!LLO&E66j@{1f&OHHnxXfq;K&&V{ z5GHzL#Nn&z(~@f&|H=Le1Dbl~Mr+Wk8#TAooGO4%qZ9AImK%~Qvo##G6%*U!hMv;l zqxt|r=v1Yv8!vq#qK?6R@E~VULF%rOzbCoN!E@b=P_>7$DEeG&U`*ZH70_xWeQ0$> z5h`G)B{)Jd6~R!dN%?0~UQ!YdTO&~dGLIWW@Np}1`)}+G)o>he&7K5s{e0^d5KG`c ze$jNvqc${q4p+I3Rl!t28@@WTVr??rJ5+mxFwFt9=-V8I!Vk@k9e zdJtMxSqB~yiy)EM1JZ-O6LgR3?ORt`#v6f8oD0*baeL>+c^9-{NQlk>v{JQAwGIeX zvg{FDy;=dXWM5t$H7@hRBw|gfc}cl#47CT+PTm}|o14dwfi@z8rwRBwU!xS-!N{j&n&+UfMP~=(HhuVoxpEH!RTTDTD z1A*=o1n;b@tQ3R9H9|GAnqN4M4p}DsfTty&EEs9|=FVYXR=A_)*<^X}m;n1%s{1}~ zpKf_v`@+|`YPWmh3T>OX&ovAE8;0Y^+5^b<(e~O$KxkucxI3a39$XdwOhZjZ+$f*g zm{o*Ys(tmVzb)c!0T%20MQNMwG9P+wmvQ(d1Gx~6gXl;A5*cFXQmrS5k}HuP4HKot zhTI&=07UUBJ?=`zVsv%HCnlb0P(iCZOxHRq_qH17TL7NzN?WORa5??zI53Y=QwM>> zb7|IrflsX2&Vy{j&`(Ru`j!cNVwjz47pyR<@EW&v#4Js^og@IEbs0hmDDRM=-0(}a zl0lgMI{XL7^DrJ#7Px+=J877cl!hbY;i1UIn7KfZ8<+ZbPb zY3nk%i%*zgAvk${95m_vj`6^#`2F}e$H1X|E%$|C_I=AD3bbW0ByULlbG9qsM-cuX zN+63)+8-8Kmooxz2)%8+s1F}_%Iog!ySkF4Tcn1dOY~NF%7oZ#;5t6~X`!5oswzXH zhGGC@&l^UKI=KC%jr7(Ket6d|@$TiLs92Z7gJB+WX~U`aiMYSS#Lm1%)jLVT#*h7F zM~YOA?Cu(;am@xDp1F&{T=c$K_~M0+3FAP~xlINY%;#_1SP>^IdHuAdlwI7J36Gg; zuwbS1ZaG?4`EuP9lhxxtu^S`$+ukYBJ+WYqfA)*;h((}k=2+5|!{HxEv_u#ZLm`01 zSTx8VPQ|?syUWT#=K%(Y7ah?E?{y^L?SduC%fP-!x^4HMSXmZrgD_V$-8*c&iE)Tb zl4U0(&Y0>+WW}5#A3uzsABYaqA!9T`qDJA0Aj~fjbc__#4T!Bizy)artPM`XdINmj zK7mzIHYzdeny3p_dtElZ$Bof@AE%cAkRjJR+a;WXdkqY}*>1LSY>GYk6yS`B{*9&e zcRpIMiz?W5t!aA|Ol2Z`vg~P7-`fp7E|>yh?HocrT|Td;c0ob}@@^8zM2xcSngrVt zC`A8xN1Xmb+RKG$tOZYfG^~G`o^5TuFry1&bCYe2r{(J6q4C3<#l6~z?ocnNHEBSm zz%*yS0=xYNm)e|2K*1-WL_VAh?kX-)CSt?eyKkDNcs^%dv@2VtUAouuWbW|MiD8-x zM-$+))qFKu-T|vrFHn$h44&wC+Pz*F^V^&t_#moONR*58FX;CAO<%Y z#tj9w_Kj?VCVNcNV-!|uS~cEt5kv)~6PYIvt*nOGBnIE0c5U|{fJflPvgHdfXL}3g zh@z1b&01}57AQM5(G{-hIrWYhrSd+1)9a>Qi8U3Fu<1>>n>g~bVd7*8!5CdJ0qEjK z_z&P?-ug}`NkpfixfwiGL41}w+2fw)FQn=Y;7_t)k*X99Y}lb__s76KxxQ>R{ zZ8-O4!m9{l=ju*9r{z*56a?BB041(ldK;GZsE-O8uKk1Iic{DawQCy`1U#wNTkSwa z0LT{S=ewbB>wK_6owM?kFaZzk~dLf90m%=^@!f+*GAO zW<0B?Qe0Hj(_?6hu~AB>U*5YP5E2n#Av^RU5ai-{6RmT(kd3N-nLtTBUCt_WHkyc# z50*tyy_A91-RywdXg!!VD6scFc#MHoa0o=Z>)-#vA`^K`U|=8=yiRtJI3u7c$xZRg zaacZ}vwzSEXoQI?tu;j~*oeMYD=WaM4Y|&+n{0M}{~mCd4wHH^J3CJh?A$u^Zs@L8 zz+F0?K6zXiNz%$+O2AZQZ4^b@?UzzOnkX7C$Pq{KCeJY$3(j|2F4~V7NJjxTBMIsp zchkd1*b^Tij3m!!`!qF|!Yg2!I*CLFgxx}2>zsb9>l^GvPjJo{8B zYr>^r)YV$qh|-$H8F#+Z(eEiFxV$r7FD7gNT$I)8Ccu=ct+5Q(;IlUM&&7a~S@CdI z;4dm!P0bvy?f9hRLqzFs#tN3-7(mCJvH4m9I6#yJ@&V2R<=YCSC|(_SpwTb_K+LSg z_yQKzz1fz?j|!z`MY-s5FTZ{=vwf6f6qGVTAvdE1`F9x+Jv6u)p6Kr{m6O;n>JP#y zr5)El|BlQl|L*MmOK;D58E)k2T?bpXZXMe6s$+Yvq;3GQ4YDI>kCghn0lX%}H>esx~3NG8G9nbs@81hlIkv!C-H2#ORmxN3%9xL34xJikGOY z<|o^)L2&0duw}&uKqrYAEny(k?!9*kjVc`-rb+ZEcp*-t)-pXIRcN|ZtVn$DfW_=r z%PkR3FLdFF)2BfX$-Sew=SOdh&zb9T;bJn6-n3UhYv>)H@Du+xyHA>xp8#o^p0I3mXWvwNg+!zbxVd0_8php=Cdh6-wdli&&rb~yM zQWAnj0f|TIWqC$k)I|8Q4mUbMxr!5PjuBW#0WoxOaXOrAOwuRF{ zMAwh_i-`UTM+OUbd~|Q-i#O?pUe?bARrDx~nHNyAQ}a4`a& zj{nBR2zWc#-C%L@>6+xXkS3d}yO@TC5V3OnmT?S;q;fHgsN&WvV1I#c0QgoTrR%?{dl*|OTEu{CUG&Z}`4 zXlu;tq&t$l%IUy?YJ?ENdNd@G$MlkE!lXYyo4FGpBd)pMyBiuxwh|+-js0cTF@qw_ zAM8iXX8=7TLeR)iTgGXq2WQB4N7!Je7_RQ`LCq*=gnCFk>-%Lv8cu3PVYQ^;)SAhl zX2(M!sKM3ynEWDgVJZS#G|3W;%3|YSo_S)zA@stCe9v9m0M7Kxnbf4OI2K-|-|_mM z&1h+5W@KDWe%I|Zoy}~W;wXMvI~CheI!i3W1{6EPBqtA#^$l8xACpGjesOLmDb5XJa zXmTV^oUttM7l7sVsbn-XI2e@mEC3c6(wrDXb4)Wed~`Eu5CQ&>H}_2K-x+P_FOlV#;e7x9;Z)L&L>vWMmu z)-&s2Cdn4Rf;vjo^YEuBql1@$faL!TwCBBkeadL?G`WJGFkdBn?H)*ga!Qdcb9b9CiL1r3MRA3CiHTGszEiffK_Ul&HQOTzR4n=2TJ-f`j>vg^U7MHtZukNKx25&($+=B-ShXs5X~ zGak?+FuXb8mZvq>cvtnK7X&m|4Tj{~9)10;+<+@ruGF5)GKUe5Hp3M&bRB!#a7OwpS(B;>EbW?_8d$ic zY+`M|ghXnD+YfRH&ZNuCJ_e|w4^m6P?ssR+%Z7I(kfJ1keaqfvCBZ}=+E`lk?%j#n z!HMdupF@1FwoFXa#ZawT&Uw;}o`K4J(e~ImytZ6CcH1}?%<(z%{-O@^9G|O4S0B9k zy}sa8^5BBSKAh*zlxLpdWv1Hm=`3|tD!=J@M#T(XDr%~XfUL{I&MvJZA}f!+(YxWb z{$*&%*Wru)l2xZ)-v}MnE0MHly%DI7-2%;bg6xxggKUHT-j?U!dM|;B zn&28<-aAN*S{{N|7~hPk^u?!Lu%OAKmAV$R9Mb716o~Ate1GCFXoZ+)oS2ZX*Ldei z%2Zn*R7bNM_MWzQ`}PfNh*T8ag*OJci0{-uOHWo(7kvXTO=WMy`82>h%cH(#P;G79 zVAi35s282Nbx2jw{#$3x-60e|(sTc+x$6mA1=P~+)}WX4o~;6R9qS_mBkx3MxuT{^NThh@k%MiWL?+V> z2}Xhqa`ox~B1pYx}(mA0rRj!iS0TQB!5D+@2p|_WiG6L*p`XfaYeGR+Y&P6?{ z3LytUx}KWxwUS!JM_f6O%m8~I?*2#fc!JxAU&TM?+8Z?(g)#@8mQ{Z(rhZk*gI{h{ zdJr9L1S&ePG&LXtYHI6UE9V|#qNfkyVB1L-fgZissU6Cf`l_V%aj+9uHCS3;CN^YP zAL~2T^(l7!LmnBiYaFs5JMLJufuM5dMlP`|K$Tu*J_9y zgUEr_5F>a@dE?(7G&}GVcO(>nCQ+5&hi(_v)SE|wT{(=NT;$QSX`lI;8Es>z_y+uV zT1hR1Vb?$yzqwbk892=K`Tb27lb+f@c`wAkVf&Rh)hIAWVnA0@Pa_tBu#yuSExf^H zvFUi{#R#fNhqvrNeSDhHjHpq;=}@nP_ywn z{P-s0I_~61VMyd}Af9tX#C=~kO+G5Bn3EKh^Z46`!B#L`Q8XHMlpyJ8=J;?#`|wer z1$M#P=W+jTR*Hk%t$SXP5P*Fo@m~7%ILS@xyO4vYPy+qz15qETShj7IvltXtK(9kl z8$&*?1Vf@mEmY(P&VFcUC}_8tD?9~|Bcd6dnVC5(O7jA$v*y49;Q=HiB{64Iy-Oq) zZJ>a;l3%02>AK$`i9Qe-)xyNK7w!l-aSRs!%~+BP{SO35u-3NtbBH-PLYZfkq?O`h z#~N1BcV%%rpwwR;Q2n`O)t=8c^7C+6?p{RhgEp#TT3YWRC4>@|2qsipYwz1v2`1rT z`|@Zd9O-0r9UWA8G+23y0VCHXAMtVF#`z=4@LW4t z?p7B8V1+Hz)6$A9yjI862B|}6@synT9@9~OJMQKjbqmez=!3v zIHBKF?OCk{4U2DC)OJprrH^jQHWLG?THlyg#JmI@FV3(Ks)h+UkZ^Ed5qoxXBRhdTN45}#QbL|A27+jOe36rhpEY1NG~1LT zAUT`?JrE%H!+rFL$~l=qS-Z$Bw=vWS@xZ59>2+GyiEkCe%hN zQMUmx4i4Q)Nvkh|^zS)_-^6bu?w0y3!osjeQfC6iK)*b20aZ3JAwO#)yvD z*>wm>$hrf-)mi}TBNcia5)xITop)=M*X#s`42Nkc+LLj*J2W1rjwLCV0s97>cO7mC z4nLBkZnOtzWnm%lm2P3TVXf;&SQYsB_&`rXe_AKO)I_jeKIzWmYgs%?BjhHKBG%cr z-bDK$u0XQwJU6)v{Q>pse=}Y;uPB1lv8+{z?y>(Lm%f3l z`EhWx{zq=rro%YIc=Ru7r0+`}1SPrs@w*Sur`vVsSseOJ77`!1)_~Xe4}*s&goq*0 zVz4F&2}6Nzc+M4{TJg`rG5n4ShO?TZNbeLl=bCuV0Uzl=?(e zi4zeU)y;TnX8 z@1FWWB(eMs3<|rT#feeKT&^q>pwaX!;RwF!QfVPd{_DQ*7;-M4=lPqtCt@(UTT3|A z)7uK>{%)0s&?#R^$l=fDC-?+OmwXHaB7eVFh*RY6HT{>T`d@C0IAWwE`QKee+HI~j zu%<-FtHBH#&6^{yNd)OG)mW=|nu*)ASduXBNQN9XxXTN*k6CaNg9%rYc9gpfkybd} zLomHwZMQ>v>i9}ht_O(6C3-?UJY{;0d#s1Lynl0{?QIF6~^w1^00s+2jWw{28+94%0wlgV~X+QaS zb;ie!U0-%gn4Ge}-Y*gbWs_6on4du?fzwp!S^IU2jdi<1o4G!I5BKl`=@U?!Iae0v zq-BAsTyx{Zc=v=so%V85rs{h+&p$(zE)4M+*v4He#S2G#Q~w$4NM85>1! z>x=XB?ocX2NdVA}W-vlmaU^Ugl^^&{Z~`3+9lIVA^00*k>ECa5I~~K#RUY6n!G%9u z0&%&nl|s4B+K**#bzRQM?3_ciH$fy|2))UI&vDi#?21q^?QGf#JeK$6Bh<&iIKLtY zf}fVQw#ll)hh*pv?DyvB)yk8TKkpft>e7?wI6^UsAX4aO zzN-;G@+t^frTFoR!-(pyWEEl}An0sZj+aFQ0$AU6_bDn=2X%D9Qg%oa@EgHJbX?4& zu*$J>;{j+s*F!E%k~1@bMPcpEgo~BB?g&dxoFae^KlM6>D*-Ohdt(bxE<p`D=5y`;!AAT+to&D2^hsdn@7Ijx`8?KN)4 zDh+p)8UAtbiIZtjqNM)P56MHvrHD0)uhMDlhfn%B&~6RnbNj(Ikh#v4(LTpgCfH6~ zQF(u6jZV?>?coeMYhI~V3A^;teAQ5)LX*$dbLxtCrE#m?;admtbbHNO+m}Uv(&gEZ|J>PRvcrUS>z#2( zM+SQ!QY&^teyAcFbP*JU@sHgu5b-BspO*&-`8`4k@VM(x#_3DIbu`la16n&bf} z=I4S>yGA6GthrX`0vF}y75nDqy8ecHIx9ELQgz{sq zc*~9CJG!TiQW0>FWd^nBNl95nK>(PkPYYJWZu(yK51q3E=2Li?>#vR*!+1i-B*)z7 zhiFvR{rJItLJ1)w+Q?b?7}aSG&!er}lJd{jx^qa|Jw&~Vc5Ax>a)t^k2)%wEK4b}k z27hAuRtJPN&p2*cwx8uuRDVY`HswniZftfm7IF`Zh|`KVn^Het+*MiS<6w|{Uj$Bq zh5j01-iSV;R2y;g8XMRgSI}!OEL~Z{$=GpE%@WZ4i;J?6KT+MdGuW13FfNwBuCSID z1u${y0P0LIFNexn_Y+6ZUm`lOl1r@y^#z2D5BY)NZ!)XgSrg&VaR<&}hv{kATc?{&E{v`o zR#{p`&7yE50-~#xh&2g%Z^Q5#Tet|RML7`Txcoeznky?oc9B49HFeju{x;tobc*Ka zI17u+8wB+DPGs6hEFwW5U>l0c?B`{_rXPB~;8C$}BqniO5sHaLdH~oi@ZtL5)d++# zK~EJw9unVA2yB8+9YS6HgsgC63qm75T!f-t(2otB#@pi0P*%m`aMXg;l@Z*gKnxH$ zi^*+c+!VF_!v0k}S~&=)u8w8k%o3fU*4E7fHI-vfOo`EVI6`yfDZw#*|4px)dhE^> zNSJE7UMd>&P4&AdL;w2CHY+#fbZV^J$b*VLkhxzb$EY6&J_t1xLUILSnM$_5K&3R$W38Cv){rW*w0e)|wYEM9wwdSwp#coMkOS1+>DE0AeN_XKM{5guANBl)GANFMi*k7*?lpPzZq zv{ccoy;PIcxT>Suoe$KG!IMw8xl9B;jj7H63y3R;^m}E4ECx5S|JRS_BbpZX{%dG( zznYpDB+>l+BS^*wr%f5LK{T@?D9Ramtz6~?$(DF=L`+Eh1>X|uR;fuvumWR$fcR{m zmn%AGv>+$~idnWXOFiu8GjHJ=?cYzdm@{YjgBUx0>l}EHXBpI#! zWQW>suScL%5ql8nQr7H`ALH$_roe|*E4c0ufdhR%EZ;L+^~)Fd+V1TgZ}D&tsl*(a=m$q$E+9k|-Gp z70QsYt&ohRD6_3WiV8&|D=N#(GG=V22_<70!WJT98OwO?cNx;K&$)irbaPm0y>$fKhI}4*LjOW{ar$9`<%Y-{nn#Wi^T)?tz#8g z+;qr6_396xhdjIFO!)%*JsGkBA`0bQeBbj2$S%t72wqBL+Ip<$0X%42agqvUbnnL? z89-=OTnmM_@xIu7rsA)2h1^dK3)vJIOJ6IzsWy8TE|#^cV7KYMum$)uQv(Opte-y( zw^F@p`p50C)B~sr!;ou7Ndx{HyYdB|j{tyRDiHKi??jTUYg`aY&Fm4#X!kB%n$6R_ z<{O-ma3BA|Nnrf=!2=_p6|lvFq`v_a8_uZ`ex$$GsB7BT&ca87m=l*;@kRiOkr%sh z>(=-FmKZMhE<&?wg?9iiPukC+yrLT3X6hG$)jm-g?#ONM$qBLQhh>EA6&HWZ<(h%k>xYht#Nu)b>_10e&%DiSQ(YBc@KWMx4!F@Jm zp^jglFF{wwCnU6325+|QSfA|2(tDA+oNIk~O37*Td;sdEPdjowWfh;-%lz)~k^Cbi zb&VGf|NZIV%pQ)PmoTtJ(RSE6qRi*GpX|JS`0G`TBgc+~J)n>@lExT?D^C$+uVAXc zO(f|)F$yAPS=P(BoW96>?uVBpT5-EcgOJ$`Rvv{{l^@{D0~sLn+=_D7pFB$4LtCe< z|M71Jz7FDmZgc>Ik8k(tdEs_@8E^EZk?atnRuTYnBu#Ft2ad6(W03lE>kD=nv{&+n%I`bx&@}ciT`~RE z{_L>pNS~|OYdA!$^Pv3nAhTmxODt!Xw_827EEW^+t+T1TezzG`mjFa^X!U~-Xq8I; z^~8;&h^WtfuN#$baMw8+QfaltTKTrQ;!W%7^zS2sZ%3$tp1L-HZ+tE-OaKcBHH&`( zz^dJ=hmmsf*<&^rxm2U0#C51bq`=2j4 zlwfMvfa%fixhQ9cdu-SYGU*Sg*gHSx1!v~bM(o^~4t`jHX?mKT?-*xJ- zWldYc!CQ?5$2}=l2X-&m5LuZp|MHiV$jCds2R4j+ETNrSUw(bHpv2ZhN`Fvr^s&W`8C7!i>kNOD<+#bTohmt8VLk zcQbnU-ASd2)2&(d6v_~vGk;}f`{`|k@fBL%`aTyQ1tEW1VK1X-mUk~yow+(@Z8?|yUr|zTY zS|geHGSxdm&#&g?&5=>88$~lMT$)Q)bNFmq_q7MeR8n;8YIi){UQ~mmb@19M+1FR+ zkKbzOD{|F{FUQA-bBsN%3q(|o8V3p{x24_lkEP$~3waA^@^g|cbv)$H=vm#N1)AA< zk$mq+K#Fnn0Lm+|pYAigx73k4<`w zMo>y|td&yLZlA~qP6$>+m`?LN@4cc~wNt%BU)6qp5AVUAI7NvO+NBd;LE>ExyI~<$ zJ&Xcd;qdFikX@D;``$5s%sr{NM*VcEh{y7rWyQ-gm+uBkM9wwW!I!S;r_1KH6>FEb zyKhbA>Lo{hwv;=6(z=oeEg@kY)1+f7aMT|YTO?LbO{08L4`34^w=eZ}Rcw3IiO)Jp zh)T%D$&m`#>cPsrL?ksBftdv#^MtYln4z5rL>CK8N>94=tTqqKKfqYJywJGsrIMJS zPpK0qy<^qWZS%(#=!LqaJX!-&> zxudV!zIYcT&xSewndghNZXSvk`4E(!>yY{(sbkxQ+b5Tbxu@=3RW2nQP42ge+;P3z zdmo>_i%1&r)7Du5)K^r?1my&i)cNJ%jo^!ylJqsg5+w?JHlT$e6eyq;k8| z`5>N4(+v+furU507D;grcx>K)aBindJMMadDB{s+Xz)50AE;(sGmfuoIsdiUB7`-S z6|*EMFk&HtO%cZo=q{sk=F9=od>uA>fq{Y@ruVYCk2nqp93YTH{`>Rtdi& zVIu&Efo7X#5);1%me~WloVIH`+m2Xy2(5no>U%(>anjOu91&9bR+Y?-Iv?^4%b1$Q z&Wla3$(|_}2E;70xg`XXanG0MM6g`~0kKN-#>J(IjJB2YDyz5hP$T&wUS+K?4|xL$ zL*Tq>kgE$RR9z#3J|zYky(tF3X|>Bp&&oCO!*Jo_H-eSh%VH%>Bo&ff9cH^;=Usfx z`Lhg*>I98R&u)jo>)P&i*8|gmU~bkyU2@ka&7W1~$P^)|Uywh*(r$d$&iVWL+Vr++ zJMPNZ!Fc{3WfixdGgv^>|5l~~9svb605hXX$>urSd~?C!di~&V$(f(&MREFj=?)6L z;0p-+1+b-y6c`tiJ;-u^9wYF2%a+^D9t@PEk|q+LpR6C>n^iE)rONhvYX%oOxO?Q& zGf==iUQDwo^GoYU74wB>;*~Cc-^73S|B5P;Vuftq;|I%4{tg_vPV)AUJ&e;;^S*@$ z{P8to?@ex!OjTw#W&W=>pm`bV!W|oU++Vn-;*PKQ!3~qEK<+Dy8>yp!+QxRE=eea5?ZnvMkWqm%?C5?L$B|dh``{8Vx4#@(Jl*f-BW1{N*xLA}}z_=2RgKIHc zMpo~aIm5p$$SeibllGuoW==;uu#}pIdvZgQu?~0DNJzB62Br~}iw#j2e@!sQUbnu6^ z+uhJot^i2*pd?&?mb>Sf=}rO=K;%&J?pt!! z&;|(!d(0(mCJfeg#G%|=rDe;wxPMqJ9y+)Uo{tp=ZGDq=aHm&D5UO-hV6 zVv}J&)pH!`CVcnE@NgXAx`+xUZv&$$0PtI}{|U!>h!q+a6nFlSclW_{hjsc+-Jl!mjSGOlN4GnS7bkY{9SE7o7D}4o`}2pRWMs z_P$YgjbCl5;1=DK}O_&X^OFN|qv|@Vi6`ha4mTtO?Rg?7)CYGl; ztPcj}rJO2*gF!PqQ)1 z3bYiMS8T;evNlKc{6PtF{Txd=MwJg@4X2Rc!to@Pt+-BTb0uu$|=7CbLY-|`>yn#Usd^N53x30g#9pTzG?2l@_uyDUE$hp zaPVMR*nze7%@~nD`Z$a&{%hM#fR=xv=l@%c1#Owz!I%Ft(bssUbNo+=u!*z=cW&ji zu{)=e4A&U^P>8lAfQcj&M_%t)^DuZsyAEa#L)2PO_j)iA;{wht=-Ogi*-3_D?o^csG?Bs3A zjfEVjzYE{+HE#>CSXU%`<{J6R1sS8!Kh;ZkNoKR>WT9--{a^JA$oR=z^)sIz1^~n- zYV#i{3+{y5W%`{OVB$Y^5|-9rH+|;{3k&m}e`h)@GJ5Zh`w)4?5S*PV1NogUfy;cq z^XK0IgsXw(a*<@qC8U-3nYjP`;33u3)zi_Z4y6mktEzFT{^s3U7a1tg|0yi#7tY0T zO{O2A*8oIHs$HAXY@$S|{QA3+V=Nze_Y@8uJ2sp&=vE#0P3J<-MF4oAHfb>6bL*G; zXA6>5O0!X5=Vb%P9mzO#eI|hS0|UZnRdcLK{sQ*$&CN}9gWtP)uUxKw za4Ym9B!RtFEIO{Z!BZamB9L@rIUaYk3Tq^-yyJcK zY5))Iw3Zf8iNvFGF6p%1DPZyvvmi29!MgCGKcpE?s?X`{`mw|F*OTwdA z0Zn<$S>K)uspESdMK%J^fE)zOnAd<|8^y}WtSiv$91!{qD97RL{HK$cQqkfg!8q#{ z?Je`RFK(y2mrE3-ER}hV@$UvM-;K4Ksx#7b~&Fpa-SJqCKR%B3vWFNV~HZv*TCIL0Y2rmIefP z#(h~w6hN+BcWQ_$Ow^{qz__#)KP6|_+R*4L!6HQ$Z&)`oV7iL|yvQYHl=OY?AT-@K|FBUS4B(0s_lN!sl4Uo-G}7;`7WC+0j%M znZNly-q<`@cipALHI&}_LWxnzrhd1fp`n>sxKZGYCHlyw!;{}}oAz{uLP!z1=hoqK zmnlT|#lDy;*k6NDRM2F*ZM+&cwW358Qx87Nqvv%E z_YusA1M8M{X7NZ65|oIw*b2p9IiL~UEm6vLxQe|hkVqxw3Diut{oh|FQ7(9qaCleZ zBNWKzQQmAb4Cq)?8bk2TXxANc7@hnq(D!s#r15cG{(gil4j-iV92~Kb3H5F zMS6fRrGf|=`0w}@6Sf;t+F@dLgF=DOG|PH{#=UX_A*N)c!IicM{G?wrM3bUSRkAMY zjgaX3GE1InLLqSehpr)QZCD~YBrHNIyX;!t?G+PL=?9MEp|7@LK3PRBqO1OR=bhUU z-y1n0Nnfo!L_`N0i=XhiE9Lq&pw$%TuM!K*s=o3Y8296Hs&yaVvWkMsK@yx4#8b3O zbyZl5%*F2~AN|UWmDKG{akTKdEGQo|fTNBoeCL_Lao1 zr@!5F8U&h{g5)~>jw||mzZWwNu`cYA^eY`mqx4+pPopRq8$Zn8!0X&%nnHr!S5tNs zP{H!!hdR3Rz}Mrx2wpX$IpV*^T|Kn-;_v5@D2L1uuI~$WwALlpO%~CP?cTle1*qf< z9JuW3efrFwpqaaEZ)qp)Sa}}@!(ry1<+*pY$OV%~nc1g%xQKyi;m;aV{Qh`j;-568 zjW?t}AfOpDHq3eUTjf3ROS)5I$$aM&5+~{ZNGbA=jLDEID5GKI44z(fwa%{jAQ9!a zq0uEHL}$Dw!F)sbi}`@Y;Zs_v3ni1=%0lvoe_x@D^(I7py>&FuN6Hmz%%ZJke_109 z$Tti~h>KVTPlbi#N%)jQNfP4Pk;#|J+CAFE5SQ#ShgLanXEgY-4(ATXEbzQ-Jc5gI z=9lHa#^e3~g+ug5Ae<&C=qi>P5N=yxCGge#4c5w+9r2l4UL8kl*TC5a+#0 z4RYxZdu%Mh;`xYXylguHH*@`AHC1jneA7?dJVKp)5`#IblfOKYv{t4_RJ0cS4povaN+dS(WLeDI>5 z(Y#^w9cLg|mJ{Y8+>V0Dw&dCCzAc0K5PX)k5xjWPM%z1*ZL`JLecHTE9P{VEv|tzl zo|vjLkdyzUm}dtUIRZf4Z$= z5f@h$OwFNmymdBPOyQfJ;rm7CO0eR3cMptm?hC{B^D39t<}dXZSnH{!=@T1T@jg;J z1Z+fG+6nSSH_MiEg0Cy}gUOKYVk55M%qzWoM#(=2=bgg?P?_#$=S|E#0};Ro^0b_6 zC_^|{Tit562p@)@HcnPr5<{X;E6_5pA86Uw0|w!cbC+%3YmY-N-ydF50cB}g7F#R2 z7&HtNT$`8M*L@BR@{cB*8BI<*N6jRrz6jleMI6IeTqV469x2hAAWnY=Co&PM|UNXUqE-C}ZQ=B4TfcT&v>5 zT}Da>%)shOfGk?%g}K7LBP8RnPP0##%x!n)7CV>Gbl-nc#epQFJ)Ja7T^O1gSE)~? zdMaN~<9eL%xili+mbjo^%1Xr;5b~CSzN<+(+wu(o`t?0YtrrJ{uO@DK+PJw~Y>S|r zZH_`o^8QkfeGN`Z?nRL;7G6$A(eU_Yo(q(;CX5IgrKw!6o>hxdyYD8*o-!TCAUim* zjP^`; z82HUBuSExA&lH{HkmHvrI1rW!o9ux%5UQ~Dj6Q=dL95l0e@P14;F+$23bEuG6g&tZ zP;)uX1al)mkAaXz=tSwKy}O#l9goQXkXe@L9>MrO|L@t`d-lu7{C!LP@7u9RQ-LRY zE?C>_YKo2w%KXpS_uf}KERxxdmYOhs7Ilm4)XUm`ScbQE}CrfZwY&(QR2@2dfJd&#`g8=pku30*95ht zQU2q@Nu!Rc^=^zfrF%9-CQDiV(wH;xtr)0W0L@FV2O2Gr&AFA`XFg5pa0XRzL_xFx zWmoqiow&GSJeR`4J^pa)HTeGw^dyU*kMzdgKV@M50ZoqHsy{)KsMSYvuHT@^(IX2z z)4%njrqffi|9~bZen~Fatz!Hux;3GW9e_(oYgGG0>Q*HIQ|fe^6-2wKwgG22vCnFY zvaP3i_y+|Y($llBp3bmRjvSrjI`|;ceSExjr+FS!gaizSw>(BVB|yRq;Z9O-DvFZJ ztNn4NYp)j;j;B(08*{|`NazfjLGRqq@+0p48;E!+fsB^i%s;pBAA@(sCvUTUHHioh z$3`ME6+149RSv)U?11BKq}sXAn7OU)>UOJ|%prJkw4sGgPY_ux0i#sQKIUT3*9tDK z5j00QK`#ik9L7ZHm$c5$n_wi`;LHSK@M7ZsJ%Bbe+ry!s_2G)s125pOMCc7G3P<-rnn z0;1IU1>KnFAV*cVna6bXO!Jfuf>s$vK`sbw1aZ**Bys9W0KZ&jIkL?%2mcQj6w~gi zgi+{jr?+1)F&DGTh7 zBTy42C+?wLGJ1gg7N$@RFT9qSEs6tI0W3mkPt_!L%aXO^`zZy!15&V*^u4m$Ehqb- zasY9RAT~?OWOyVFW;=7ieDXL9R_@}PKxRX;2$XF+qj8=|e1ND&`)4n;JBSuWWXuoc z{jo;YHNJ@|y!dTn6st(<{l}JvK<+4C7v*|d-^DFF0-n4>H>XRU5Ug8jWAiFJf`Pn znjdLDYw<4C4*;K9;}_q;B`-a^DVpzOVZN8rO4>GnhAC5}5e>>Yk~Vc_!(ylM{`sm* zn$!1ARMF+^hAD&77#9+MiAg7ve%a-~2QoRUK4lZjEMqUYpov(NePO>0GypwU%WB)* zt>DJ{!5wGR5${2CMqw z8O)r6*rIxXnjl--ME?RQt!FDG=1NLiFxt*P(3X@&vC1Y=O$kAX#m9U$S$X?#J%Zep zEW)Sys^W$tQ>M-)?d+4vUBOoQw{3z=$gp^$sVWH--OZ(WUh8360|7O*W5WDvEM+Mt zj(D!WpuA@8z`Xt0o%Cy53;c`Cwq)k6hEf^dQ1-fY?t++qaXCUVM5oR zu4&!AV}~KJ6qv|5>6G|KyTTR#LS-H9aBL!CvbUBep-$-|N)Eu(RqS0VO>(jja!it!1mRger0jP!Lw?`%^%&vAFaqijJ8cV!9+^@%A#oBb19zBdok(KL_sQU#ek>oaI zMB~XHtNxJPAMLbj)sLY5!dDc14u0tw1f9b|2)$9?oXU`5E7T2GW zg`utoU@8EWX;jvVd5nG}^FkbQJ47;ZRG11)E?S5u7$@`>*A#|ti(Sy#X9wLGpaOBY zdSbt%th@AN{G+}^00uyV!Y2HPirS*{pH+O>RJ|PoNiR5w=YO*wg+rev?tCZ#Hs;w$6-xgr; zZOXByf5<-2p3RIXWvWns-b!qF!lc1MchZ~w)Z@&&Mc5V696?+?0vuR0F~?iPdSucG zIi9(38ToGKwNv^gV(nNlgKo%SPtUG?b?fMVLi!Y;Ua&wiNB1$AoYd@ivkvM?K|wnt zYwn6;xd};jqDc`>axDD(ORx8kv`qYhsM7w>)BQ^zo4Bc?W-FsN@u%-Q>ZdX30Q#k; zdoygcGz z?WMZs285+|QFJzf+*sB=5DUGGb_DtXPzsSr*vn+HuUvJChp+Xu$$;rYhb0*Zxj3;w zDq~o+ay19c#mxftV7e46K~`xSB$%q9Gg<%z*(>=0IJqXkzc6C=LYRx>foK2)bzhSe z@)~%Pggz@l@;TO|5ni=e=AO#YbKFfxY^80R)pKrj9*2P#-23|>;>PwvYsl^k0_)dH zqL~_46lBt%2Nl1l@R_x*bO;I76J_qRX`lb3e>1kF;G2 zgzAWb>iQPxLw50{)Tzxg$Bc6(XB%*LVOxjc>#se0zt-PQH_lsJS0rQ?-izrXdcA_w zlZ&+Bd-$C0aI#wQ_Mz4UQqPAo@z22`a{(uZ(V5`@Yi_chg0LF7O8N_KD7i3D<^s?9 z3so~EoIE2x-6VZUUKGQZ9IO^e>>4|Uc8n=EC+*gV7qC;B4qzz8fy(yCeo4u#S*53C zkOEj+$!yA+)2!Ekt^s%I4NTuNIxem z`!g<+nS4mS{jhUjK7i8x{otAhx@$ysx-P+4RErH}1PO! z$Xh@VNl6W%H^Oh@x;FUG0Q*2z-+&g4xqZ&p7}kZQVLjr}24YszCCcspT-UQ1Vj(ht zbi!^}js=S1~fNYr52hsDz8E5CA@a6$KWhfz8_s-$Sz3JT(HcO*n zVFCm|{@MEBas0DSRsgq+GwihtetR(9?;ma&A9 zUi0USJAnbUAHv+vJ9q9>ik-9m``wyJo?4U8gQFQu5WmV-Eo_6884&N?_Y*Hnl4egr z6C{KjKJ6{Ud4-7*`%O0-*PJp%=BMp2P8P|ZEIc4C|2>V8xZ}X|66>GWEl?tONBxix zO+(PtEv9ouaPtE6FrHdizZfHyCcS99sQ$$!>*pive@J-!cK`<7 z!_xA{I6vMh+gu;q2PX)EumWamu1t>Uy00+W{d(Bi_C2;aGWJT{mV9iE`koeKu1USk z8f1nQo>fLh;+2#Yx7WeaC?%z=n*B`ada0@I#OiJ0`^;vbUKk6$6*PDe=~_J|<2bQU z+X_QoetdrUO*BvrbnL}q>|d*F9F9aq0hIDIE7*mrsrlrJp)&&PirW7DhpjV8z$Ea; ztRsplhnlGCfnfFPA=i?j1Snv-7iL7R|Od9ej?m<`|&KL;JNKE>m}c+_&LB)U83Jz34{bYIoAb4`lo#A-j34%3@`Da@X&~Z3X%a z>6f8lEfNIvw4SpmwD!rc%#rBzx6B~^MMgu~)EjEiVL}{BR9-W&=d9xRJ)N*$Lr*6l zJBjTPcs$R~P9n(Wg7F=C2e87Ko9~#yB}5u~p9A;Cac3#b$nYdZZ;X7+YOcu|nFcKi zT19qKQ@+07cynqKhP5cJz*9s^^ag%@K;LjTa6j=mgXZhYwv1rdI!Q;>H8sVK70FG1 ziuN$<@_6HPDH9? zZRM@-%;T6h#gO@pzg))Ytgwb)71XlGZbLsQm z&t4Hj;SG7iuXcOx37EeL&Aw##XQIVrN@wSNe~>3;(Gf&v26R#}39^~NHfvXNQcIS< zn8;N^^6Pm+=E)YtJbvqAp=o4fq-R$K%pB}3uR0dg>sByu-wkhrs494Bbx1qY$sU-g zh4+*ty<96PzZ5h?zt7eiyjDV$?c(<@=wws>eE63HMIj}PLJFqjzx2}QY_+c5Cd(ls z}jee%ZQX+WtSZ%)XzNNLm7pO67OYeF_LG=*&k?GBq3^A0En=Yx4q}k4yR` zebv%Cah_cLyW=0G+j8^*am_iA6Sw=a*9mZM8{dpByG3Z#J3lm&iSL)9E;V+ERB{?U zkD&xh=b06g4nC?noZ>=5m16;=e0vWlWfuPhG4N=dVP@ibhaa@6V7tk!==n#@-uqg8 z*o!llC|F>AAqQ&oMF8=5v?eh(Yu3UKu5_d4yXHL=FKWR1ev;lr)7~UlPs?67t8~;d z&hzowB!W4T>*l;IQS|)*VoUR!ha3NNp<9zL8=%(Bc3F}@p{#`F$N6vwIaTcj4@deSI9%V{QD{e2xVNr$wxiEzkvjU{oxH@|ZAc)X22 zilbmIO&y(7haNMDgW6~6kzSTlW|qp4M;_)c+kaAIOsBMejE0IE9{ z7U<&SM!P~kzMl}4zO>&Fm6Dn+luJi&XyHbC%v~0o6zekNpzUwSwi7-dQKyU0%*!;S zUw{U604k|OrUuoNoa&9aRfp@bUW7MqhDa+QAV3^PbF;kadULXXs(NLHl8$4a0H)h^ zG=I79Yu%U10TR+rN%+W#B?AMb=1N+iN}#Qjw2(z%FKk& z2M@lSma+T*t9-alCXc~%QE^aku55NKPUNH- zd}d|YbM+?V0-6}!rKV=sahx(4Z~8{+QDtTCf_u>@KKI<*_>VLO(yuHBT!7mFy#<=D zTb;ix9?#|m&Cq;|*supbd-twDgy=bH0XA}t;{DE#WNU*c#Ql-CIj}Hb64BGlY%8Sh zPS`KF{E*>0R+n6q$0HOdIs>(J31rpErmXAV@DnIglD8Hjp(JQ6b%xi=bffK`XYZK_ zkYkzgzavLF44#C9w-1fv$xr3wWigH3v2(Ww>vccA0C&BGd}gcH?1~NEMb)^xVeySe zEQJ*g59J@K>&k36%$wl7vxm=rp&TDKyKBad>6>QzFe-5;|FzOX_tjw5+LpJ{b*-xKUAy-05G|B>_7XnllsO56&U(P|M4AD;?9Gx2euMTqAxy&BH z$Nw(!NZaAK0(PKfzOWh(8+8-oC?uV4=eQBwvg9b8qz;kGmipN?6+JPZW{7KWhT$Jz~5G8u6K ztq=D)WXay)T95kC8}jPaD~L+b4-*8KIngnTRm;k;#<>r^)>dd|L}X8}TF*)hhHZSg zoV$JWq)vR4gKbyCo=7INHUQ8o*&#y_vFG6a{Wbu0gP;`!2&~bDxP~6B>o-#&^#PL)P$%3ji0QVEPaGhr zau>8sL{{c{5^pOY+dg;KX;e_ME=Til z7g9dQ_3rKPmmF}ycu#H~9>kpHIAeM1cM`7*GgNHwiJFW(pPcjeO5jqgLdd0zZ^j}o zMU|&U(Dy_|__iDE6U4+zcyUM^nw}^nSV{d{kP2GLCxqXT0=Z#341)o^3TvXTS+jUX#)ClrSS-2f zAno&xB6Txa+W+}-oNw%Wdhf$}Jw|QDzx;1*c9%p@FvUYnOCxDpV+5r+oeec>*JK@{Mazly3?IBT$ZH?&JoOR<57bxe=$qTTm70tj(B-et)2q*gyf}##oOMz) z#{FGM3AG9|5>vCa>yTj#{KL$}Q!ZNW``FeHG^f&m) z$x-WgPRz+yml9+6BoRQFRA-UKji@MW2QeHH+D-#`)i=YpvoYM6Ri-M;e!JW8SrCi($m7OQUWhz&Dnb5651%8mOEF^|rM0xxhb>n} zP67!km&6x@wE_x~{jBQoS)XEU7`dsEq8&3?1SdT?dSsDoe4wSTuMZ>45u23>!6!AR z)LsG-T3f5SP*9abh8P|mKCYE`zPA;vQG=om%YXF>_Mh?ATsk)%&1NF##-xXVh%n_z z1N5%PF{KAXX|}@$8>2A@805Z-t9>e+^6{f?U%;Jnz*#aM=ML<>7*TfwI8%`8ipDm1fzkjK`TtIV2)7wH7yz03x}je!}L$9c_fwLU;b)8 zgRude-{I8z5IUj<1=nh|u8s!R<(HmL2T?NmZ9cd91@;7+IypJn*lb=W!q}?erc4|M zw*CAn5&Ap!W%fy7urHHoxZ4*6k|9id^v{pupoT_6<=^j4=JT)AqsQ}!H?zeIjcLyq ze#%Vd%a7$BI3Uoh&cLu!cI@iNKL+Ws8aI*rWR2fx{QoCHzmltxWlESrq(KVuQKrI|$BI3aD?Gqds} z%R33^gDo{XcKz*%+rjHt6P9^Q|7&6K8mZI4V|mrYUBktfBrvm7@|Y?pyg)N&q<c?iBc`wtv|(u2<%bSMFP7MDYUa1advSqeS@0c*=+ z!o!;ZnIm68@`*&^Yng$`btVUNNWi#bm(3$RebC@2+%WfV?|OH!uUotWW0xXHg|uH? zS9btjO0SX@$qcpCJj*7l#O)r*_F96Onl1lvpN?p7-5VmJix-&`JppsaIdE8hfD~g} zFvNMbiP6!5n>P~|Sx(&-2Uyh)8z42f%C#K>#+t|}k1)}HB3*l}fX9tH%1#<>s8?XD zDQ@nnQapbSS$E#_MX0|}Mk`836hS$2-s{=p zpevI~@(SgqTD)R)>GTVNK%oWfG5wR+rE0-g>^4g5{XW?BHj1gDX*lrF3Nm7rfqgt} z6|0-}3`*~gvbc~3LOuRk5*bJx9XeL7JAoM>pxVR3!j2`xrKjr!hb({DqA-$0B)9A` za3HE*(;V^upY!p=;JsLfMI~EvNEIiq>N+6)HI`w|J1<4%s4L@H`^1{xrBGnvtE}N~J2!Wd#8aA;m@x zX`haV*}+uttWLz?>CUP3Xkxl|xJ{}POl$jtwGX+yGlCZ?U5G7CFi)M_8 z2EAw*xM;r7=oL#R>QM%U1i`TrvKuoz6!-1Z%qiyHK-#m2&s$}4Ta`up8?j2m*eEf) zxZ)ChKfPjVK6YkNH<~vd*ZV31?EJ{Chv@D!2U@sE?^rT0?HM|+Fhd(`rwZ0PC?p)T z8@gc8d>3YIezNra`=drikznMLvy~Tg#CfIxmge2G=_?{Z<_ZQq9*QEf%+5btst6@U z&FXMb7awE0#c3!cQ!@TGp5XXUiVh@AJ?`zcMv8IPq$7lW>#AcfQEWD&NddV( zWHvzleMxkcmqeMMayIv6ArB+Dj8iB6Atv2*)SL^jTf`ME3g(N`92HgRx;dV7ZZbvac z{+$McRh@N_ET7sBAYt^D&kJJGL9q$u0Z`SeUea-=q@r4c%F3mjpmh)H)m8z=MGG6=fSK_FCS9sgzsx+L!H=_7XY|FUw ziSs#W2LjuZIPb}M_IuqdhbNt{qYD{1_Yjs0xK0j9JA_Ytk9izq{zhrjy>2mh1!BsR z`3N%VJXzHk10_ahtUZkZ9ETXJa{7J%1n(a|9)7EJXnK!GE$8`!t8j?7J`0*HH?X%# zq_3$~L-k3|;9dtN-bwJ!#J+QYzv{vCGbaEBBaGA&k*Kr5^eW^Hsj1Ebb{$$W0ULB5 zRiM+BmxYvo`aNP9f2)Li;%r#S=D>SC{B&uqpV+A;)zSv?UFD(e?@y->zFjI$wo&Z@ zGQ0G=Whp1Tz_Q6#+aziQ>!9lg_VB#mH%@>;Q z#d$Du3G(xA#XuJe40_rsDyq>|eBJR!^=CXVTK&axQ{Q@iPur!E-STko7=7c;z|b&9 z>u+)eAnKJ;krFiH5tx`f>PadC+Xz%2jo!`xlWh`dw9XeC)?Vmj^Y$jK??xI0Cl6`d!RRw4Spgl z#d{o>mi_j@hHT`0k-MM4fe7xm4-R@~z8A}4IET$IVR|Bzj3odNH_Afy$i~Ko5pM4N z{h>SYM~9uv-6x=2lzxerv6aw=>dqnEptlu3nci9CFMN#jkHGD5izi1A9(F&>0;y%? znocToBOX1B#iGZ@zzbS3jc@J*U^T+}^{^q7m_QsCx|8X$C5R~2R3Hs1xKbiQY~CY!}gABPX5sn z*^LtGp)|S(SMs&qFZ%E7TYm_mxurl_Ma@mQt^( z;vb4>zH-@zRKnQ z<{eB!MT_~7Xz^yD zp0#Q;%gp{#>jN=O>vN|wbuAZn=5X?##e9Lh6?Tu%wji7D3Zw8$=D7?zuXJ1yA3GVc zJ$5NC?y7zB<>~ol16x+Ri(O%w{bc!%ljG;#pHg5PdLf>I z7@K1F?Tcy7Vd?Z`1}PhlaCzikrquMmOEw~gi+xpL8UXM?!Z}oH1KI3tvvLOrcyJdB zQtQ5|kjJC}duBRl182$f_5=(CfS&|u_;Tv|^3i$un@zg=0_|GtuSs^c&G_K)U!N#f z@Iq9`9ekM!6w9=>$|^?wdHJ_RTZU&4Ba^nYT_=xy3{EYEeQ*2%=Xg`G&`LG-bT9>f~#spG3E*yqE1MiDfVE%do0%evM&cD{~Im?3R-| z_hAr~k{Z4r{NG*S@4dPbRK#`lUV_?2yfj@w(1iv;$kI4#b~Oa4FjmDK@z@5}5u5zv zYKMqZq!bQ}&)NGrP--A7G|-nYMPYZorL&%fCbW8B1cs}`3!OI!4JK`f_rq64&}{3{ zW`4+T=?G>tY|y6F!}zdM`W5<%Ej^tY%iB+(en&3rJ1w_M;O~N(e}+ique{XXa#k>H zL!(e7neT|K1YCrGb~p4+WE#vvwPw?MhG-bKcImnUX$do_c__v@w3{<^E;}!0Unm3d zV8fvtEdn@U%*cmrNJ4GTcY3G1c@@Nod0JmPNi(+vxS8++XR&G{B6*oZGcyfRAt^6I zw}S108#4g;NQC}_c`o8_%O5P^;);QfwmnVH8HD-J3Rhq_%`44ZOSwIbQA)NFgJtugMpDYEZcvL{Kn`t0N27abKDDi1J)83r$t}L7^4d2;K#)%H-MxLrC)eU%PkySp~ofDHjr1kHt@_& z+ng*AT=909ZAq2VTg~wVq>sY%1i?usczoroV?Bjl2%0OB^CcT}=rq@D^Js~fRj?50 zU9NB1(^~T<5ui1DA<60!;h; zOhcZ%(k!gutS4sF}cs-9u6WWj=yFISw#jQS@7D>oGS4t|c4SAAP<-Sip61mNY6nx?vA z0`qie@$peZK(jkiTLg2?&}zGE<)mNsi44m7f%~Y)y0ZIZDF3INs~_=9Q(W+*yg5G{ zV(j_0ec9V`ac0z#2R9JO@>(TY!d*k;wATZfW;cDU`ugW%_K{ox7#SrdQqAG3qR4Q> z^w_(NYu({uXN-}Rtn+HLPBq-na%tduQ~Gr9^H=zQur&{N24sD+6W0nU7ifh?;6JnL zMwcfk&kKqYHP!zr+ z2#khdV{6VZmM$|FOwPcBvv(Y=&~4x8u!m%m!SMc%mzXMB5sZ^^{@}H2#@-XV=^4dS zmN_;zpGeO~()yin5kImR1U=Oy^;b+_=;Y(nL+@>vCj*^*LPcg%)t%^IY3msEo6)reAaXhqFwbcC) zkO+wqZ7{?-n1Ca8eA|4y=;W%={P+U%Rf^!N6=T8wlo6vPR0=SAZ>TH@ce+yZl*wV= z-W(HYk+(ZQZv*|}z1VM;khn(r^#m|c8?nxh)7_h1MEvs2NzuCuw%1_s3Azv%I1JmL z3e$5*mNI~j9xcE1N3R==+EeN%@M+kyqC<0D9{x}u! zF&0NbOZW0A-;SSAUXGvQ*)2ZyGVT!z}qLA<0(o%>%Juf23#?)2;K0Y+S zZo}&#aKfp72qqnpb2-HJ6xG3AY;7e-3{!l|R6{6b8I$_^`vIyT7eK2^Y+)VJm(E9b ztiq$BZZNE=ASH#fFmK8TD&Clu}> z6&Y_;X{6Vebgq{2`l$4(R|0JLD(Ic{a=pqc%u!eurjaWBy?()sQVz@R)6%zpBK7-w z+ah|uy4)NNoUL~7ARlvaDoNqf#I&zXlWwish)JyI4J%2Sy$!Hpvl+-tBcpl<(TxXa zd4go5>*@r^-OE*lQoNT5e?sh+c6=}r|JOh65PODOcGM}G_|++AYH0DA!dqBVb1^Gx z9J)No5Ewyu_gjqNfd-_h>ndf~s{P)Z)jc`RVGB2)sfU0$-O{orNE@G)WJB%R)vF^9 z#_dd0{?gm$qSf*>_iICJ=SEnXT9(|sy2d@1Nv#cdL{gw)hsWoRFc$0duQ0%ua$-k> z`!{Ou;(9mcfh}{BnffuODxwWM2wKD1cM1V^d;a`+k%p@3Msz3>MSFNeba-T>*Oe<^ zUZ@Z5A;UjdWd~x`clUtUwa6_NWg7>+VnD>oTbh=aF~w3(UbWn1lw%=tbRXYeuGF@Y zRefqLrw}-#Uh>GQ%lEYFe2l1AjFydnG)*BkhoJl4zGO2)c&w?byZ+W*&di?qxBc!XF4oOc=Sj?-x(}d6jL~iEDn#-H~R-fzIvxD4!Q%3udGo+Rq<4FGbAW)y9i+; zEAQT}ER=ByIq91~sY67*=ebXWO(`^fibi1%=QBfj!52HLLW=zN{m zE5SqU%kJ*Ow>A)HP&xZlV-m{u^VvPOkxA#k0l%Q2AflRH2DAUws}9hDo7uO!IPKZv zhHUtiWpPXO>F%l3f$9 zn*PV2&|2#!AI{Wycd{Hlas++}i(5*y92sw{mEjx)yP3|>@ONGz+%RII`yJES*13K; z%LN>H*OiUO|J~73U7^4+bWa6_XCD$4hbNughLS;`Lflr%qJ%i4oKo%~fg1gLm;Do4+>(S$E5;{l{P3r`P;e z!DxrV?k97;fRXH>!4L7f+8d}z^7Hp8E5~Q?D3xa~e&V$WanJ!y7-27@7>Q03u_f8FN*iCZ21&tkQ|^H+qi@lL$ygA5 zX@5VEBm^?+XrMh{q$vzvrCPtxtLY4sb)#ue)2J%( z+`OD1-)(hub)rI!o5$6TzXx-oBdj&VO~v|cbSn3oU~GQNhTM|Wen=Z9XqsQP>Cf29 zQt55(sVh&5j8@s#ewI}Jo{mlVN$>c-ihJ*HF84ov{C!R|s6-k_$|{5oqOwx9PzfP4 zNC-s>8ApQ>vNJ-mXJlkI&5X=sRg!F3nQ!06O}ozLx_-a!AK&xOxvsp|ec!Lw>$x7| z_j^m^dPU!E0c6y!{VDOA>n815drWHiN|e37wQ+vQz<|xnmt}B_-})c72%kovDJ^iC z3{7=_)SAz%hmj7Dl9p}teoK^BL1c%Dn%Yk1Wz=ppo9#Zo=81Us9?rZaD>;K3!qETl zKWte70nSBj;=A7k)28L;O8|-pOkqAVZ_N<*VR*aZMWbdjaX?+d&O1cRx;k@#S#(FF z+*-k3RyaLr@0wDD0WdQ1m5mfAFzES!i6#)7Whm^zkpmHh*no=E8!JT|v{=wkyzt-9 z-Skp`H+i=jYfLdD?I1jB!Sh;VTf&eU6MHd0J@(5+Mx6@wz1B_lDy>7$Vt55wr3&d^ZD zUb*LA$37N#6U#Xog_Ua8IaIZ4IC&om`(T0(r7tauYw& zK0)Z-^>DuqE@0X9==k(c2|WSzbtJGK5XN+#J)X+9z-DwPJp2%7T04J#aP%IapTm59 zNe@@mww1VIL}%TLdErUn)$h0dYqySQwR*B^Xa%Sd+6gRDb+R%tS51=R<6UpwY|cIx zAH7ikt$o#WS3+7V@5bgSOkU=*xXH=a4f(5?m=e5_6_HVP+>Mo+&nPz@9S1ixPRTQ&XJ96&URQpr z-{oRYB02P-=XT?kcLm6}MBUeo$u2~E*(Ux13iX`#r+eP#vBea4ulb%ThB16UXXIW3 zeabH0(+vQ6k_Q2u_z5_(%(DyfvKh-q!El12X(FJlskeHM=UJ(Vm?CDKDT1_*wYOXOGj&+tDl=hWym#eeK^X0 zCKEbFMPpi3vZ}35g2exDW6)l#Ms#Vnr%?N7+Ayym9!MlUC_3TwTlwjr;GKN_a?U0 z$kaG(e;yV05;ffxREE|@`jloP)??!yVFXmQK&;1VDCAnR`Ey&+$tCm4jwtDg81l}d znh69n@ZrdiiIf|9h3qBa$a%Eu90qLLyxr!?&sA=iSHi2SR`diM+7BN*7?HoSc)b)l zTxU^x!uD)}(7LSq$Sn07B>6222}wy6?X$&R$v~o4C+did<~KuyQ!WD&*f{rnnCsv- z68ec_*fNWjuB|#rF)Ach4N>p5nnAuc0<{Ru z>XvQ0a0Uf~JCle?MHU*6%Oi>0@e8;(d_wqZHs+KyAsBZyMuY^3gK83o6&^#KJ&ZCaa_Ye-j`BCqnm^yKAAJ}Wn)qTn9i1pH z^^;i`?S{@S-xWly+;ubzM-#erFgbcUB^qz{haASVG@>D!_TYgP$d9J z9G-$e%Pa5QyAHGQ;YvvhhyArbmoP+bMEnwsn_>8NBoVf_eV0U=6S>rP9SR@eBL)r- zK+=bGIib9QVEbk`Jm%+w;gdJvtEag|2SqjPD}#yP0aAdy@XwzI4!fr|nh3G)IPkF@CPcdcjq`ChO?_U1dl z=$CbZ0&|YF@?VJ{ejhQi{g^U$VMH!VB3;?|E#6@ky!F{1a?>x__zxVRpBEJF&^(Uc zJ!pg*qY6!=m}n^mzc{15%k=I@)vt98F0b>Ug2&<;r+0VV{Uw(rAXiBp-mNHqF9m7) zR(`r3ws;e8;(xdcfg)W13!Mb(1o1{iRn-CO!X23!x>OMn5eP%Qy}j6HWrPaqLsTlY zWI}8HMbnR28JISHW&tG~i!uAc$yZTXnOPlG`H4W^I`{p+N7`sie`mVLmaQOM+zUAuWK_l#}0I}qHkUA` zGMIuEH9;Q`+44KK_tnQwsNk)U4k0PC#{r+3$a)gMALODP*HWL(LzrqSRBCg%H!KB; z{I`chPdcqaM~hK@^jve$$`Tn(wzj6`8U}_eG;A8bBq&3O{z!I83Sq1iaRz4yF>YvN zn+b)Q&EDYc`^{`w^abM9=?^7l!?Qzsl~h@2DJv_hAHTjmp*TZHTFmfmNo^2QkR||Y zbz{GPhPt>nZ@w*HSc;%xU#G4K#J8-R91EnmEZaekPcY8`2uUHrj)`!j0?lcx-2%LE zfyCFV2{X?dVW~4PBz9vKMXM{xa?cK*bQ;ozSKl%m$3b@5GZ-LKiOPf6fgU zYfjE61mcV~2)qU4C0=-S<4;YNT5uQf6=VPp?%|y_MN~i>A^iK#^Vt7r@u|Y(%eG9lZ^n$DK z^OBt_wjZe4fD<|e`919A0l;-(v^Qk5_*Fc~|F_*ogn+$9$~N7Gl^Eqd-v%$f_1jNx zm=F{iuWKjxhd-YpXjIJILK#l+9z#N0 z7hqpN^-<3t8Eqse&KY&HA5sD6J@tTj(#-q&3A=osuI>zg=19=MX!sY!N9xFR0CAO- zJJeBUNzOk_;#h~6k`Hzp8<4#i`2O>cCp{Hu(D_?mr zk$;j?d^CKs!x|w9A@yAs@BQIiikV;`FLcy6TR{+8vhUx&4@LOQ1&N)(>bo9-Wcl+s zHg_lgf5o8x)jc5s#m{t+pTCtjJrR;f;@{4X{|ZUJ&B>TQ|Md>O8P>vpYrJXti=_PK zp#uMwd6>~}UlP-vFB8 z4hT^KXIOUGi%t3~jup)TiJ~@G*sddF<#EVk>K?vIs^i0tklF*8lq9zwaPz+`*rdg; zAv~vWt=j~JyY#ak3-=H-S|Fi(_eIJ-W9>*U=BYd)b(o8(&Ve?ziD90|QZ zxRM>cSH$uQBjiTd7rF^k>2gpCdAPXti!N}K9p)5IHgc%%A_^IxnAZ!`tap!w4V^>G@5=Xek0?)3}tfpn8c;I!28HK`tZF340t{4BN)T+r);lfY~@-)ztSDmxW$Q#p3x zX%*HCZiT>5Tz0NUvL5BP%9F>5`#Xp>!46;MsA2HNX&s*di!+~F7^{eg2p(qMn$LLH zXs~oQU1MK&9^e-Aqq^;~Al>hA3PV-(7L0`wkojY&`SzhQh5lmL%f;N#l0Adbi7jJm zhb^pV;t9lbHbg#9J1I>;-xus;6I0W~AY#iVK7T~H<8ZOW@)o9F*@o-}iW&!X=L@db zPe|1w;L+etCOcLpl~o69pGpoHlR zGT0UD3;BxQ?jAdXL)oT+W`*Y0;6p)jKVP9hvvD4N|9f^En7p41p*M_NBfdg2|8f*G z<`loObRNO}b3^wUMEVjE2q|17e?U<{z!Kgf+f!6+7K{IJd0(QKAwK@n_~65Z7z2XJ zxB+jt{Z=IM3nKI909O@DTWiy(c2(8W+j|e0Vujzhvhf%G#xKW_0mTd$#aPt zGivZi5^ip4)VOzppf8Fp&7k~F{rK&zqe7QD`%unF8Wi6GeUWf6Pd!Y?9HNGLga!&$ z&8Bf`%P=k!JNQL_+K0D#@@NUohaeK^;_inV775{xpJ)%J%+p8|6QS z8nQS8#mbAoMKCR)@rjS*YtW#!d=xYXQV1Dd@a#V^o4=e|#f#(eH`9?0JN-u&f9+T9 zNO4)&FSL$pG&EW_L~MZ#aS6LSqNGDW6A4~L)wRIP+cXx^Rr^w%OOMD@VHmGZsr(!G z^YpX9TZUN!iY=pfJ2034LrOjWRU6?85EE^hIdJF;qdBZ5z67G!dpN$ZRp{p-nfOd6e^svJM{-W8Z;`}kF|p#DQ#a&l#H@gdmxSUCH0 zwjN)3p()i+>DaM%ph4I{!wM_m6miXF&*tj33T~Kfv=tOk>~CAnubf-N+0O}fAPR*3 zra1`hi3iT51VJS3$7uHdiXeg?f11KJhPWRi3!cr#GxQ#vPZ4rfKqsV6<-XdyQBXuA z6MSHx0AUJ1G?NeJNbgg)j0#Rob9iWoeGwE};2)X_Srs%H(O<8YKxevViSaveNEG)$ zkq&A27z9z~0%;B?0|X$U^F-g0M`piLGl=JmQk`jrv15)@=>qH#EhsFE%+sti3aS~zNdt8RuP^QAOr&~Ihq*+bLhw;5|7hl@a+QI z5useY($IyJ%?U3NnPk7St@!3QXxdAf6&A3-sn$h%0h1+{vHW&KWj{PG0MXy<-A0^nV*Z!8ia9Nl2t;Tws| zj=Jw$I(ReehbWf#?>vNFdH#X}X;vg_J^0YnMn3F=sDpOk_mFd(2sBy4xt{D3QcdxCV@K+;{2?@p97LWB+TJb!4%f zgmy3gGH-wX&L#@dTi0*d#QZN*cxUivK3F@RFg2k_jENcd^@IS~pU9{m71GjCL;^Ry+>?kn?aW-;v-l8E%W-yoiIPcWn7&*BND zIzQLfb4n5Eynjd*UauyaA>T{*xt!zw+b?~@n~zDn{`9*NIhL6THjfwWbiHNAJ<19H zOgqy-^@Hqd{iDQ&AIln+=ic5bTm@;eS3rMxG&xhO;j8aRDY6Gji`rmpj9L7#6b0p* z)snLl-c6ZMeqZ;NF0hPcc03x6Ol<-e@3Kl|2M-6(2y>hEMwS;X+-=cODml}CA^Fv! z1#9>u{Fz0^wj3H%YWs_R9WLMe)0@j4jhhbhJVB6)3Zj%E2l)$()y*`E}AF48fl?qbf;ls_4 zpdcJnM#)ocvmvM#kQxD}!#_(XE>#~J&A604{pz@6Cd~rA!;E>wCqFUon!h7kT=zJ< zg%kyZD|_ZVqMl%ce>{HX`lgVNNb2_PT%&vTC82K>L~?wRx_xBL%rXBP z6a(Q7A1=F<23|65N}*+!P$~tGL+Smn(3Pife}n0YmWUoIdyJDGjA`ZBq+iJ+@NQs2 zBATMFm!YSq&R$htUk@jWM4}gOGa3kz1Ys;mpd^DlPz^$o$a1Atf;)p?VY>sb4YCE` zVYldJkMxQ=$e5d(pE{NB`NAXMcn`A9fl<3H(womp@8eT0c(~JP_3G96FxnxybueS$ zgy9IF9q7@-BrRHhaaVX3>Y@#K5Qen>5MXq0>N1VQX31d)O?u^KAA3#}psa^mJuAve zy1&KT)zv;lNyh2IOG#qoE9a;6y;@8c%FzLuIS-eLu>Ev9i$gn8+b#9T_XJGF8C{9) ztIMui(@8jda@8jk&Hem{eUxaB&@lEK4De z^Ss=hr!n`Pt(=|qIA;)v+^D(>81_Vop(ON1k{$f-N9OJ zpVoPB#k)Qi%;sg=U<~@~nKan6wHcjB7Tz@ms(;@6B$mGuj@nb*-6&p{3 zxRYugDevxkK3zvmqs->(^D;-%7f-ltyif!ThUix6PmtAA$eG?Oyi*@{cy_!XQ#rT} z{jJaz(!+IS@2d1iTy?qWa$U)D$H-8mS&{6my`~pw%#gvo&a~iX@?!ry&{hCqaU&V> zKM5^SxbbiWQr)wzysXT->NW%kUI|rGN=_0z|m@}BpHs! z(9q6N75j80*fBUF&gn)Nppt>OwDiS%ZBz0V;E&IglG|}EZ%aXF3x1a^5WIET# z6ueqyC$sKzu*lSv(aKBr_iK6cn~4fLN&ETu>z)tTLdkI&TP99+4gs3j!VYA)QW65F zk>pe(DivtdNRGc*P^rllv#w9fjN%UDz3Wz0X$wIo8EA4k#X6=la6+XGW&ThZ${(-a zH9D@vyC|ZL?V}6rJy67Kd;2Q#w%h#ua*JT#nKKp?1T}klsl!;KQFk{`(uuPyL!V_2 z*O5Vmnn+vVDB@7|PQg1WW;Uh&Rm$gJ z4Q`avO^h)TP_WdPBDea#k=Zxe&Ngc!0-WStV;`0kQ)CH$p5)-cmVs7d3(3yN>nr7l z-C24=&z1|^V_1_ZT?Op%NT?nh$-y6vMVST=>b~>af!kfBd3d8_WW5gkhl)E*U?70Z zm5DPFy6QB9n{~`sDAexsr(QRNz}+0`fB%%`0z2oJUsr!q8d30%KiXs)v5GUH~Ou_7DLI@eXRLJ6W z;&1$Y$d(z!F5d68!qf!=4`jI`FziORoxsFQ?hFT*PnWrIlatMKvJA%(bR5NpUkKuK z>MsCCuMOD;0Q8!==X|q$C+}^+c55VX%AZq#=7pmUK*L5D{+Kw+YL?aC5tg|}d-ikx z(|FB-cs^p)cZRl=WNRN7O0jboP};|Ka`*0VF*mPU>aKg2(={1XpEjp3n^3j6w^IHV zAGCHPW(Ne0KA|wyM-2^Twa=|vw>&*RIZIAU*%-%HP3-ZYc8$h!x<6*R4wl|c# zXX4!Ef<*o-_F)ePsO}rI=IW32PAnTR968sZ%qU6jzLA{2M|O2AbUED6Aa(DzZqSu1 zEx&VWrhm9 zLOoDwqq@eDi?6j7Gw`W?+nATk5(d5oJ;h3YzypXK+oN% zbyRT*A|wdBT(lo^A2|4Qh_+3*FBL3_!I?U0?^e5BO5DCa9HV4wOm{dAjbRvszYxgP zF!TC*MCoxxuvUI)pF`3l5eyIxX`Xb?R z7mi$~-0OeQZJ={07Nw`8`|G)hiz3h4$Xz$tbrk#{`o$lov1W&ACdcajWS^etO@ zMjblIf(#!j_TC=ooK~rL-zqvg>(JjmJ3H$oN|@6L3~XyHCXiDINdcyJPLz>{1Dv!0 z;U&}9pH5HRXC-q;4gC1Fvh<9TFBOwBJgS4U>u;6G9Z-%nCr?KfdV$7w&$NN1jM6+R zOeKBVxnm*4CpZR1c2WKPaaW*vK0_c+OV6}+Zib4g;9i|*29+7dC{~VeR!U08eU^c% zdDIGxLX{!#>ydeebh73HH^X8>c40CrPhQs2PyyD`SAxf$4GWxL+Ea0NPf9~1%kPrlUd-u$cUk$zxWQ&9efC%iLLf6}m?`N2P@ee2QEiLaNA9#_IK4m~|H2c}n(=H7=OF`ikj|nMrETu#5bDfxDqXT{2VLY^(Y8IGm4E zhRg_zVkmc4a-xK}$lcBm?^k$GIoeUYotLS$KIC=EJ?Rr`hhDp>t8_Zj6-t}5WM8u8 z#kL)`m!zzt4>!L5;n1r+1)VMDty#1MVe*5&CK>6s!oh3rGdOkJJ zdDesp?LCoMn7oLlUh%j=_&{BTL+>kvAybWdI_tNTluhih_Zv=1U#xWVQxZ@pS?|^~ z(hN82TDd8z z)c06aSfIAg>;aQ(r$;+i%$8+dtMTox%U?a~Y}_OCG?u06U<3u%W)ej}rEyAwfT7Rb zl&~W`$|C31?i``DXeVtrYObL>bM@xq>IX~(>m|wOJl4-1bC(rZ2s&McqUAqaI=uuPifc?5}Ye`SDy>$Sg+RfRQ zCQICS0x6Ho{5)u37%mbuIZsk5FUfqlOVByso5mn(=SgZuuk!${q%>s5A1zA^L?U8I8wU(x$mdxKB6jL0M zJ4>br_~*8Mv*GmRVpCJE6QLqn6~mPt|AcbLZQb~GqlH?hM%M)OT++*>bC_=D>+%Fi z&!*Ya7qPA^3IteL3PmL}EG#iX~d#g!U?~2)HT>U0Z^sJ7pVpbmyOr$ zl^RXo%oVPtx#IMqcSaLqgVvEf`mT0YMtAJB<*iv7ZNE$3lP&_LB?c1F&sz)sN!Cox zZ^|6-eOv#r*=;Mi%9L!fw4_kkSGM@pDKc|P_M|ItOh>z_8%j&#U{?3wO#6xBSza({ zvMs)pogh#s7GPe=9qJ%H`a19~4MRJ;uNWEn0(GJ)$3!w$*nZrUBjHi}_UWdGzp!gx zYg#kSu4P$S(*fg}N>zy+ht!4}ju+*d)O?83)NeVGpdJ{}*jdgk=N=~ZvZ;2*k!4n_ z9G~L#m-Vzcgn2nlZu$B4kI^6GjIx_5oNC_1Sd~8`sU^wO$o0atVO(9HGFHvVyzv10 zgC-A2K9SWs)7!R-_o}Ie(zaUNlZj7AIE)QA-*Ck6_3bAazcTGc*^D%M4&$|y!A?7M zX)V_;XBawC`e_Q`V;4w2@INXsv&xaMVW-D4;v9cjL8 zkbk}HA++L*WCNL&vXCQgs&;j0;aLl}tvKesOkI7Msz{q4o2rLfUftT^n?XjLsw#r6 zsn_h*RX=Oa;?FdxgVn8sqd;FDP1p*K5pUgSHdgN-Brn`7+E#CNx%H-1S3ec-K?>P+ z=wx$Km(P)tW7ZE!|1nZh4q0#lVKF>hARtiwU(G}2&qlReq8?8kKT0zba-pfi&cL^S zcdB&2a+T1Kw^j?-D4BOy2Ip<&Aa3-zir`ZL4x2bRm#w?#k>#c~b;tbCFvDAM5$$kL z;uN^bgCFP@{PomX@*m>^-A0zy?DmXkXbf zU($hnGpmjUJ9~9c4?2qkaRq!iacos$QA3q;dXATnJfGAv@GfhLll>B;r{rsK0W_}Y{b)>MjLcAH3 zTyFPiNCF-{v=_<)-98ivUK%Qqvg#{z8z|eGX8YM8Lq()KMQZo8ZqfdaW|zoDCf$UQ zQ_uzSO%};?FcSoldS9m}-yb~A5MEtVa7XA>(9vrz=puG8SPTtQXPQmD_tn%WohGBd zOp~xTw9mqPqat>LiX1Rjy9*m`-)1Qf${e5hd;w|s7*;Eze#?IB1}2MBlBODan3k_k zVBS7x&QNxTZfWwcgoVbA`bIDEx~)$GoftU|+l_9MrHbQ!7W-JQ@B4A|!9NuqReo#q zZbwZV@cZ1_v{6mg*zr`X&%J_+9NgTwmJgG&yB8_><(lx-ra6p!T+*O>VP<+l$@NC5 zumyRaz@bQ4!6&xnX&V5$EGsGLYIATj@WPNPWb4t_*OpP1vg{Mus8c!yu;4&le5Owo z$kJTOr3d5B=##i&G;L*Wci#27YBIUGJl1rXX{Gh};1a9X7Bkyfm3b@0MHG`HjH5@E zT3$E)60k+~I7FK2x#raj?_63_Z+uNnGAvep$$_eY-bS^ZhNVlRlb?BZz<(Yrhwd8E zo2lTa=j7bLA1Zho{4?vHuH4zoGQ@*aJzJsZ(uYBtslf4Q7)zOu@0xJ}+tz@!|UwR9z6zZQI zlXQ4Of_Btm-5m0(af6Aeex_rsXGaQ|1{7@#jvhYjVOXY-Y1&Z1^?1?|u76w0rM_><_fNouMKqG~=j7 z9ANhGpJqb^dqw51GYATd>u)BBm#5u6&4u&D-Nly3McRuOa=jX)k&_KcDXQWf?L0h}*RsiqLAZeDTx?y=WqO}MNqH$I1H-&y zGt(RG4r(?yj~6Z^@lw~kt3sMrMtg>T#J1IJU8B7FUkJ{q829~i0EleJHw{KBux2EVBy@YVv&2Y?fZVgZssZcFGX`usuotCMhdV1T^wjh2 z*zr8S#%Vo*<%%_HHf@V@JN?&}3q7eprL8GPMYYSBxV$!AE9qux9NbKD!d}D$vGefr zf4(T@x_YL9_y8rp1g~s_g$Us%lo4T9W~HcJANla&L`2x=s6(_SW!U4i?IU*w{C2SR zUqzDR#>P~Fkjdu7z#{DvFp+8(O(`C+I+h3XVzVpM@={A?FVm+#E}x$8`!9m91(A4Ey}B8VSX->f_nxjI(K+R#xH(8=Z#f=7;x2LDEB*)?>H?yoB=-FG} z9ck@uYFfmnajK1T) z_CS=+sd~CpdU_Wj-VKziF#8ZRib~c_A1n5>kh~L<)bC(oB2ihcD+#3{@+?$;9hHto zgrP(e5L^Qt3+ur*R{*7ItSNTfqv$OVX@K9iky1v;lz}VeZg#c}Fbl(-{PN%pR8;sC zrAoID;(8}JQNEppFUJ!JyM72jD(t;vUK5pA~24hrBU<*%oRADcTP0 z15_sOo;Kr+E2Qh`557Y$e+b~5E(Dk@5HV@EvZO}%)aJoi?;8e6jd~#*mA-WE+NTtZ zKQxufO_eu42bmx%rri_b#c4Hl2?c}RqK30OaN;@GhxD>aXH z=#byuK%bqL##g4grKE`Qa2eN54GbKlJ9uE9^o|3>2v=G(lq+T%9zdo+3AX~*K#s-z z08eg%tOQ>(e1EInVy?}DIAQS0Dg&yxXP#`El$3ag;2FX?m#15N5d7h11qM0k;Sgml z^&}uw8(!mo8~0EFC**h%8}`axgUB$8yitt|7NAd}_=aQ@)#fB`PsvODd+T}veXVNa z^c>*qG2V1#>DOxhuXq#m z{{2gwjNo286gOUaOn#yzr;X5bPq*1%GIu?swu721eXqBTLAxxY(@pAT+w_M3D9Qu1 zA6K_v|K+*gadHr5IM!kzo@_;R^&zQ79$4@vEQhvpIRO1)@_d)j#<@9re)`$2RCr0y ztPj3#`&q)4k>RwwF^$-*4f7$1&Sm~?KDq$5jaBEJ3(ucDW9O8}G_I*{FDl50_OZVv zrm1+#l8CY6nCYeqY?w^Tr6+uHJjBOtPao98Eg?NV4W-hKGR=|xmZJ7gM1TIt6=No0 z`tex>nbgQr{FU8}4>EmVv+D?I7MEJ$LKj8eEtGi|KOIAn2WZo@Pf@oCK4*hGgk|;} zIyBzY#@S|XOQU+verBQAWhthLY#|f2ZG{Ka3B(B5P>afjMZQo09dLe>a|xjl0#c=Nh~GYc6DS zCbT5R^ay$(e*R<_Oirt>FQY57A$|!xWYyY7Z6Ls5D9cSO?)@Tvzfp2kkb+L=@bIwj z)l_Le9%^hq63H{xFd__jnhMJi-ZI0)Y+{v=R# zgu?0~Vb>_Xqx%5yhFynbG^i#4nT~?mS?3<_2OKfTwfzi(G0Mag(`Be;1#N0|Htzey9bZ9EaiqY+uBk@87 M85QZI-AA1M56;Pd6aWAK literal 0 HcmV?d00001 diff --git a/docs/CPS/CPS-XXXX/src/Lace_Fundamental_State_Sequence_Flow.puml b/docs/CPS/CPS-XXXX/src/Lace_Fundamental_State_Sequence_Flow.puml new file mode 100644 index 0000000..895fac1 --- /dev/null +++ b/docs/CPS/CPS-XXXX/src/Lace_Fundamental_State_Sequence_Flow.puml @@ -0,0 +1,57 @@ +@startuml Lace Fundamental State Sequence Flow + +actor Lace +participant "Provider Server" as server +participant "Cardano Node" as Node +participant "Cardano DB Sync" as dbSync +database PostgreSQL as DB +participant "Token Metadata Service" as tokenMetadataService + +group ChainSync + group startup + dbSync -> DB: queryLastBlock() + DB --> dbSync: lastBlock + dbSync -> dbSync: convertToPoint(lastBlock) + dbSync -> Node: findIntersection(point) + Node --> dbSync: intersection + end + dbSync -> Node: nextBlock(point) + Node --> dbSync: block + dbSync -> dbSync: decode and process + dbSync -> DB: write() +end + +loop Poll + Lace -> server: getTip() + server -> DB: queryTip() + DB --> server: tip + server --> Lace: tip + alt onNewTip + Lace -> server: transactionsByAddresses(addrs, sincePoint) + server -> DB: queryAddresses(addrs) + DB --> server: transactions + server --> Lace: transactions + break ifNoIntersectionInTxSet + Lace -> Lace: Assume rollback. Remove local state and retry with new point. + end + Lace -> Lace: Extract state + alt onNewAssets + Lace -> server: getAssets(assetIds, sincePoint) + server -> DB: getOnChainMetadata(assetIds) + DB --> server: onChainMetadata + server -> tokenMetadataService: getOffChainMetadata(assetIds) + tokenMetadataService --> server: offChainMetadata + server --> Lace: assetMetadata + end + Lace -> Lace: store(state) + end + alt onEpochRollover + Lace -> server: rewardsHistory(accounts) + server -> DB: queryRewards(accounts) + DB --> server: rewards + Lace <-- server: rewards + end + Lace -> Lace: store(state) +end + +@enduml \ No newline at end of file diff --git a/docs/messages/client/subscribe.md b/docs/messages/client/subscribe.md index fdc9b7a..19ac518 100644 --- a/docs/messages/client/subscribe.md +++ b/docs/messages/client/subscribe.md @@ -11,7 +11,9 @@ Sent by client to server right after establishing a connection. New clients must "properties": { "type": { "type": "string", - "enum": ["subscribe"] + "enum": [ + "subscribe" + ] }, "topics": { "type": "array", @@ -28,9 +30,12 @@ Sent by client to server right after establishing a connection. New clients must "type": "string" } }, - "required": ["name", "network"] + "required": [ + "name", + "network" + ] }, - "public_key": { + "publicKey": { "type": "string", "pattern": "^[A-Za-z0-9+/=]*$" }, @@ -61,16 +66,34 @@ Sent by client to server right after establishing a connection. New clients must } }, "required": [ - "payment", "stake" + "payment", + "stake" ] + }, + "config": { + "type": "object", + "properties": { + "resolveTxInput": { + "type": "boolean", + "default": false + }, + "assetMetadata": { + "type": "boolean", + "default": false + } + } } }, "required": [ - "credentials" + "credentials", "config" ] } }, - "required": ["blockchain", "public_key", "signature"] + "required": [ + "blockchain", + "publicKey", + "signature" + ] } }, "timestamp": { @@ -78,7 +101,11 @@ Sent by client to server right after establishing a connection. New clients must "format": "date-time" } }, - "required": ["type", "topics", "timestamp"], + "required": [ + "type", + "topics", + "timestamp" + ], "additionalProperties": false } ``` @@ -101,12 +128,16 @@ The signature verifies ownership of the private key, whose corresponding public "topics": [ { "blockchain": { "name": "cardano", "network": "mainnet" }, - "public_key": "public_key_example", + "publicKey": "publicKey_example", "signature": "OGNiOWIyNGVjOTMxZmY3N2MzYjQxOTY3OWE0YTcwMzczZmVkZmIxNDZmMDE0ODk0Nzg4YjUxMmIzMjE4MDdiYw==", // base64, SHA256 HMAC with your signing key "cardano": { "credentials": { "payment": ["script...", "addr_vkh..."], // this field follows CIP-0005 "stake": ["script...", "addr_vkh..."] // this field follows CIP-0005 + }, + "config": { + "resolveTxInput": true, + "assetMetadata": true } } } @@ -123,7 +154,9 @@ In the case of Cardano blockchain, all the relevant fields are placed under a fi Below we list the currently supported subscriptions: ### Cardano -This is the format for a Cardano specific object in the `topics` array. + +This is the format for a Cardano specific object in the `topics` array. A client can subscribe to multiple accounts at once by providing multiple objects or one by one at any given time. + ```json { "blockchain": { @@ -133,7 +166,7 @@ This is the format for a Cardano specific object in the `topics` array. "enum": ["mainnet", "preprod", "preview"] } }, - "public_key": { + "publicKey": { "type": "string" }, "signature": { @@ -164,10 +197,32 @@ This is the format for a Cardano specific object in the `topics` array. "required": [ "payment", "stake" ] + }, + "config": { + "type": "object", + "properties": { + "resolveTxInput": { + "type": "boolean", + "default": false + }, + "assetMetadata": { + "type": "boolean", + "default": false + } + } } }, - "required": ["credentials"] + "required": ["credentials", "config"] }, - "required": ["blockchain", "public_key", "signature", "cardano"] + "required": ["blockchain", "publicKey", "signature", "cardano"] } ``` + +### Other blockchains + +Any other blockchain schema should follow the same structure as the above Cardano example by applying these: + +* Group blockchain specific properties under one property named after the blockchain (see `cardano` field) +* Keep chain specific authentication fields under `credentials` property +* Keep chain specific configuration fields under `config` property +* Define a JSON Schema in the documentation diff --git a/docs/messages/client/unsubscribe.md b/docs/messages/client/unsubscribe.md deleted file mode 100644 index 539ecee..0000000 --- a/docs/messages/client/unsubscribe.md +++ /dev/null @@ -1,36 +0,0 @@ -# Client Unsubscribe Message - -The client sends this message to the server to gracefully terminate a connection. It allows for proper cleanup and resource management on the server side. - -## Usage - -The client should send this message in two scenarios: - -1. Before intentionally closing a connection -2. When the client application is shutting down - -## Message - -```json -{ - "type": "unsubscribe" -} -``` - -## Server Behavior - -Upon receiving an unsubscribe message, the server will: - -1. Acknowledge the message -2. Close the connection -3. Release any resources associated with the client's session - -### Server Acknowledge Message - -```json -{ - "type": "acknowledge", - "status": "success", - "timestamp": "2024-06-27T14:30:00Z", -} -``` diff --git a/docs/messages/index.md b/docs/messages/index.md deleted file mode 100644 index d82c442..0000000 --- a/docs/messages/index.md +++ /dev/null @@ -1,157 +0,0 @@ -# Message Types - -This index categorizes supported message types in our event-driven communication protocol, distinguishing between client-sent and server-sent messages. It serves as the foundation for understanding the protocol's structure and functionality. - -> [!NOTE] -> We use the schema to define messages. - -## Client Messages - -- [`heartbeat`](./client/heartbeat.md): Connection management/ keep alive -- [`subscribe`](./client/subscribe.md): Define topics of interest & authentication -- [`unsubscribe`](./client/unsubscribe.md): Graceful disconnect from server - -## Server Messages - -Server-sent messages are sent either **periodically** or **by trigger** due to newly available, relevant data for a specific client. - -### Message Partials - -Different blockchains have different **periodic** events that are not related to specific transactions, but are required for either transaction construction or necessary to derive the correct wallet state. These events get represented by additional top level message keys (message partial) correlated to a chain [`point`](#event-sequencing-and-synchronization). - -We differentiate between [Ledger](#ledger-events) and [Network](#network-events) events. - -#### Ledger events - -Ledger events are specific occurrences that directly affect the state of a blockchain's ledger. These events typically involve changes to account balances, transaction histories, and other financial records. - -##### Examples - -- new transactions -- smart contract interactions -- token transfers -- stake delegation changes - -#### Network events - -Network events are occurrences that impact the overall operation and configuration of the blockchain network but do not directly alter the ledger's state. These events often involve changes to the network's consensus mechanism, protocol parameters, or infrastructure. - -##### Examples - -- Difficulty Adjustments (Bitcoin): Changes to the mining difficulty to maintain a consistent block production rate. -- Halving Events (Bitcoin): Reductions in the block reward given to miners, which occur approximately every four years. -- Epoch Transitions (Cardano): Periodic changes in the network's epoch, which can involve updates to protocol parameters and staking rewards. -- Era/ Network Transitions (Cardano): Cardano has transitioned through different [eras](https://roadmap.cardano.org/en/) which add/ remove or change certain network properties -- Network Upgrades (Ethereum): Implementation of new protocol features or improvements, such as the transition to Proof-of-Stake or gas price adjustments. -- Slot Leader Schedule Updates (Solana): Changes to the schedule of validators responsible for producing blocks. - -## Server Message Partials - -- [`genesis`](./server/genesis.md): If applicable for the subscribed blockchain, it serves static data used as part of the network bootstrap -- [`transaction`](./server/transaction.md): A new transaction -- [`tip`](./server/tip.md): A new block was appended - -### Cardano - -- [`protocol parameters`](./server/cardano/protocol-parameters.md): Epoch based message for updated protocol parameters -- [`era summary`](./server/cardano/era-summary.md): Era based message for updated slot length - -## Multichain Support - -Each server-sent message incorporates a unique chain-specific identifier. This identifier serves to precisely indicate the blockchain and network to which the message pertains. - -```json -"chain": { - "blockchain": "cardano", - "network": "mainnet", - /* ... */ -} -``` - -> [!NOTE] -> The fields of `chain` may slightly vary because some networks require additional metadata. -> For example, Ethereum-compatible chains, the `chain_id` field is particularly important as it corresponds to the [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) chain ID.
- -A client may define multiple [`subscription`](./messages/client/subscribe.md) objects (topics of interest) to receive events from different blockchains. - -## Event Sequencing and Synchronization - -As described on a high-level before, each event server-sent message includes a `point` object. This object is crucial for tracking a client's progress in synchronizing with the current tip of a **specific** blockchain. -This type may vary depending on the blockchain, the following shows a few examples: - -## Schema - -```json -"point": { - "type": "object", - "oneOf": [ - { - "properties": { - "height": { - "type": "integer" - }, - "hash": { - "type": "string" - } - }, - "required": ["height", "hash"], - "additionalProperties": false - }, - { - "properties": { - "slot": { - "type": "integer" - }, - "hash": { - "type": "string" - } - }, - "required": ["slot", "hash"], - "additionalProperties": false - } - ] -} -``` - -#### Cardano Point - -```json -"point": { - "slot": 127838345, // absolute slot number - "hash": "9f06ab6ecce25041b3a55db6c1abe225a65d46f7ff9237a8287a78925b86d10e" // block hash -} -``` - -#### Bitcoin Point - -```json -"point": { - "height": 849561, // block height - "hash": "00000000000000000000e51f4863683fdcf1ab41eb1eb0d0ab53ee1e69df11bb" // block hash -} -``` - -#### Ethereum Point - -```json -"point": { - "height": 20175853, // block height - "hash": "0xc5ae7e8e0107fe45f7f31ddb2c0456ec92547ce288eb606ddda4aec738e3c8ec" // block hash -} -``` - -### `Point` Uses - -The `point` object serves several important functions: - -1. **Synchronization**: - - It allows clients to keep track of their current position in the blockchain, enabling them to request only new events since their last known point. More details can be found in [02-Synchronization](../02-Synchronization.md). - -2. **Consistency**: - - In case of chain reorganizations, clients can use the blockchain-specific point to detect and handle any changes in the blockchain history by handling `rollback` events properly. - -3. **Resumability**: - - If a client disconnects and reconnects, it can provide its last known point to resume event processing from where it left off for a particular blockchain. From 516eaacd09da6a8db4aea29c121216c1379e0fd8 Mon Sep 17 00:00:00 2001 From: William Wolff Date: Mon, 22 Jul 2024 10:42:40 +0200 Subject: [PATCH 02/13] docs: add unsubscribe method --- .../CIP-XXXX/messages/client/unsubscribe.md | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 docs/CIP/CIP-XXXX/messages/client/unsubscribe.md diff --git a/docs/CIP/CIP-XXXX/messages/client/unsubscribe.md b/docs/CIP/CIP-XXXX/messages/client/unsubscribe.md new file mode 100644 index 0000000..04b8952 --- /dev/null +++ b/docs/CIP/CIP-XXXX/messages/client/unsubscribe.md @@ -0,0 +1,36 @@ +# Client Unsubscribe Message + +The client sends this message to the server to gracefully terminate a connection. It allows for proper cleanup and resource management on the server side. + +## Usage + +The client should send this message in two scenarios: + +1. Before intentionally closing a connection +2. When the client application is shutting down + +## Message + +```json +{ + "type": "unsubscribe" +} +``` + +## Server Behavior + +Upon receiving an unsubscribe message, the server will: + +1. Acknowledge the message +2. Close the connection +3. Release any resources associated with the client's session + +### Server Acknowledge Message + +```json +{ + "type": "acknowledge", + "status": "success", + "timestamp": "2024-06-27T14:30:00Z" +} +``` \ No newline at end of file From 8dca51e8dd91757734bd6a448fc9afe1aa670e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 22 Jul 2024 14:04:02 +0300 Subject: [PATCH 03/13] docs: add AsyncAPI definition --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 428 ++++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 docs/CIP/CIP-XXXX/src/cardano-service.yml diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml new file mode 100644 index 0000000..c07a595 --- /dev/null +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -0,0 +1,428 @@ +asyncapi: 3.0.0 +info: + title: Wallet API + version: 1.0.0 + description: >- + The primary goal of a well-designed API for a multi-chain digital wallet is + to provide the data required to construct transactions and to allow deriving + the current state of the wallet while the set of subscribed blockchains is + continuously extended. This includes aggregating transactions and on-chain + events to present users their transaction history, current balance, as well + as chain-specific features such as stake delegation or staking rewards. + + + The proposed API design offers different endpoints to retrieve the same data + in order to support a wide range of edge clients, in particular clients with + an intermittent connection or that are bandwidth-constrained. + license: + name: Apache 2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0' +defaultContentType: application/json +servers: + cardano-mainnet: + host: mainnet.lacev2.com + protocol: wss + description: Cardano Mainnet API + tags: + - name: 'env:cardano-mainnet' + description: This environment is for Cardano mainnet production + - name: 'visibility:public' + description: This resource is public to all users + cardano-preprod: + host: preprod.lacev2.com + protocol: wss + description: Cardano Preprod API + tags: + - name: 'env:cardano-mainnet' + description: This environment is for Cardano preprod + - name: 'visibility:public' + description: This resource is public to all users +channels: + cardano: + address: /v1/wallet + messages: + welcome: + $ref: '#/components/messages/welcome' + genesis: + $ref: '#/components/messages/genesis' + transaction: + $ref: '#/components/messages/transaction' + protocolParameters: + $ref: '#/components/messages/protocolParameters' + eraSummary: + $ref: '#/components/messages/eraSummary' + subscribe: + $ref: '#/components/messages/subscribe' +operations: + doSubscribe: + summary: A client subscription + description: >- + Sent by client to server right after establishing a connection. New + clients must authenticate themselves when subscribing to any blockchain + events via a signature. + action: send + messages: + - $ref: '#/channels/cardano/messages/subscribe' + channel: + $ref: '#/channels/cardano' + onConnect: + summary: On client connection + description: Event received when a client connects to the server + action: receive + messages: + - $ref: '#/channels/cardano/messages/welcome' + channel: + $ref: '#/channels/cardano' + onGenesis: + summary: On client connection + description: Event received if the client is syncing from the origin + action: receive + messages: + - $ref: '#/channels/cardano/messages/genesis' + channel: + $ref: '#/channels/cardano' + onTransaction: + summary: On a relevant transaction event + description: Event received if there is a relevant transaction for the client + action: receive + messages: + - $ref: '#/channels/cardano/messages/transaction' + channel: + $ref: '#/channels/cardano' + onEpochBoundary: + summary: On epoch boundry + description: Event receieved on every epoch boundary transition + action: receive + messages: + - $ref: '#/channels/cardano/messages/protocolParameters' + channel: + $ref: '#/channels/cardano' + onEraTransition: + summary: On era transtitions + description: Event receieved on era transitions for updated slot length + action: receive + messages: + - $ref: '#/channels/cardano/messages/eraSummary' + channel: + $ref: '#/channels/cardano' +components: + messages: + welcome: + name: Welcome + title: Welcome message + summary: >- + This message is sent to all clients as the first message immediately + upon establishing a connection. It contains essential details about + the server and API, helping clients understand the environment they are + interacting with. + contentType: application/json + genesis: + name: Genesis + title: Genesis message + summary: >- + This top level message key is added by the server as part of any + synchronization process that starts from the genesis point, also + sometimes referred to origin. + contentType: application/json + payload: + $ref: '#/components/schemas/genesisPayload' + transaction: + name: Transaction + title: Transaction message + summary: >- + The server broadcasts new, relevant transaction data to connected + clients using chain-specific encoding protocols. This approach closely + mimics the native blockchain synchronization process, ensuring + compatibility and efficiency. By adhering to each blockchain's native + encoding, the system maintains consistency with existing communication + protocols. + contentType: application/json + payload: + $ref: '#/components/schemas/transactionPayload' + protocolParameters: + name: protocolParameters + title: Protocol Parameters Server Message Partial + summary: >- + This top level message key is added by the server as part of any + synchronization process for every epoch boundary transition. + contentType: application/json + payload: + $ref: '#/components/schemas/protocolParameterPayload' + tip: + name: Tip + title: Server Tip Message Partial + summary: >- + This message partial is added to all server-sent messages and represents + the latest current tip of the chain. + payload: + $ref: '#/components/schemas/protocolParameterPayload' + eraSummary: + name: Era Summary + title: Era Summary Server Message Partial + summary: >- + Part of any synchronization process for every era transition. It has + similarities with hardfork events on other blockchains but has been + represented by its own message partial. The primary reason a wallet + needs to be aware of era transitions in Cardano is due to potential + network changes. Specifically, since the beginning of Cardano's Shelley + era, the Ouroboros consensus protocol was introduced, which defined a + slot length—a specific duration of time during which a block can be + produced by a leader (stake pool). Initially, this slot length was set + to one second but may change in the future. Therefore, depending on the + slot length, the conversion of a specific point in on-chain time may + vary. In order to show and submit transaction times correctly, wallets + need to know each era's slot length. + subscribe: + name: Subscribe + title: Subscribe message + summary: >- + A message containing client's interests and preferences. New clients + must authenticate themselves when subscribing to any blockchain events + via a signature. + contentType: application/json + payload: + $ref: '#/components/schemas/subscribePayload' + schemas: + welcomePayload: + type: object + properties: + blockchains: + type: array + items: + type: object + dditionalProperties: false + properties: + name: + type: string + genesisPayload: + type: object + properties: + activeSlotsCoefficient: + type: number + format: float + updateQuorum: + type: integer + maxLovelaceSupply: + type: string + networkMagic: + type: integer + epochLength: + type: integer + systemStart: + type: integer + slotsPerKesPeriod: + type: integer + slotLength: + type: integer + maxKesEvolutions: + type: integer + securityParam: + type: integer + transactionPayload: + type: object + properties: + outputIndex: + type: number + txHash: + type: string + transactions: + type: array + format: string + resolvedInputs: + type: array + format: string + point: + type: string + chain: + type: string + tip: + type: string + pointPayload: + type: object + properties: + slot: + type: integer + hash: + type: string + tipPayload: + type: object + properties: + point: + $ref: '#/components/schemas/pointPayload' + protocolParameterPayload: + type: object + properties: + epoch: + type: integer + minFeeA: + type: integer + minFeeB: + type: integer + maxBlockSize: + type: integer + maxTxSize: + type: integer + maxBlockHeaderSize: + type: integer + keyDeposit: + type: string + poolDeposit: + type: string + eMax: + type: integer + nOpt: + type: integer + a0: + type: number + rho: + type: number + tau: + type: number + decentralisationParam: + type: number + extraEntropy: + type: + - 'null' + - string + protocolMajorVer: + type: integer + protocolMinorVer: + type: integer + minUtxo: + type: string + minPoolCost: + type: string + nonce: + type: string + costModels: + type: object + properties: + plutusV1: + type: object + plutusV2: + type: object + priceMem: + type: number + priceStep: + type: number + maxTxExMem: + type: string + maxTxExSteps: + type: string + maxBlockExMem: + type: string + maxBlockExSteps: + type: string + maxValSize: + type: string + collateralPercent: + type: integer + maxCollateralInputs: + type: integer + coinsPerUtxoSize: + type: string + eraSummaryPayload: + type: object + properties: + parameters: + type: object + properties: + epochLength: + type: integer + slotLength: + type: integer + description: Milliseconds + start: + type: object + properties: + slot: + type: integer + time: + type: string + format: date-time + subscribePayload: + type: object + properties: + type: + type: string + enum: + - subscribe + topic: + type: array + items: + type: object + properties: + blockchain: + type: object + properties: + name: + type: string + network: + type: string + publicKey: + type: string + pattern: '^[A-Za-z0-9+/=]*$' + signature: + type: string + pattern: '^[A-Za-z0-9+/=]*$' + cardano: + type: object + properties: + credentials: + type: object + properties: + payment: + type: array + items: + type: string + pattern: '^[A-Za-z0-9+/=]*$' + stake: + type: array + items: + type: string + pattern: '^[A-Za-z0-9+/=]*$' + points: + type: array + properties: + slot: + type: integer + minimum: 0 + hash: + type: string + pattern: '^[A-Za-z0-9+/=]*$' + required: + - slot + - hash + extensions: + type: array + items: + type: object + properties: + name: + type: string + config: + type: object + version: + type: string + required: + - name + - config + - version + required: + - credentials + - extensions + - points + required: + - blockchain + - signature + timestamp: + type: string + format: date-time + required: + - type + - topic + - timestamp + sentAt: + type: string + format: date-time + description: Date and time when the message was sent. From ae1bbb49b1d235974d0d8f33b6fc6234c860b4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 22 Jul 2024 17:58:26 +0300 Subject: [PATCH 04/13] docs: remove old fields from subscribe message --- docs/CIP/CIP-XXXX/messages/client/subscribe.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/docs/CIP/CIP-XXXX/messages/client/subscribe.md b/docs/CIP/CIP-XXXX/messages/client/subscribe.md index dc85924..3edccf5 100644 --- a/docs/CIP/CIP-XXXX/messages/client/subscribe.md +++ b/docs/CIP/CIP-XXXX/messages/client/subscribe.md @@ -122,8 +122,6 @@ The signature verifies ownership of the private key, whose corresponding public "type": "subscribe", "topic": { "blockchain": { "name": "cardano", "network": "mainnet" }, - "version": ["0.1"], - "publicKey": "abc", "signature": "OGNiOWIyNGVjOTMxZmY3N2MzYjQxOTY3OWE0YTcwMzczZmVkZmIxNDZmMDE0ODk0Nzg4YjUxMmIzMjE4MDdiYw==", // base64, SHA256 HMAC with your signing key "cardano": { "credentials": { @@ -212,12 +210,6 @@ In case of an empty array, starting point will be the genesis. "enum": ["mainnet", "preprod", "preview"] } }, - "versions": { - "type": "array", - "items": { - "type": "string" - } - }, "signature": { "type": "string", "pattern": "^[A-Za-z0-9+/=]*$" From bf0fc05b10ccc5aae1e6b52648d3417c69019bb9 Mon Sep 17 00:00:00 2001 From: William Wolff Date: Mon, 22 Jul 2024 17:30:51 +0200 Subject: [PATCH 05/13] fix: authentication by crdential hash --- docs/CIP/CIP-XXXX/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CIP/CIP-XXXX/README.md b/docs/CIP/CIP-XXXX/README.md index 07d2f0f..a34c9ba 100644 --- a/docs/CIP/CIP-XXXX/README.md +++ b/docs/CIP/CIP-XXXX/README.md @@ -107,9 +107,9 @@ Any cases of server-side failure trigger an error message that is append as sepa ### Authentication -A newly connected client authenticates as part of the [`subscribe`](./messages/client/subscribe.md) message that is sent to the server to synchronize a specific wallet account up to the chain's current tip. This message must include a **signature** that verifies the ownership of provided extended public key (xpub key). +A newly connected client authenticates as part of the [`subscribe`](./messages/client/subscribe.md) message that is sent to the server to synchronize a specific wallet account up to the chain's current tip. This message must include a **signature** that verifies the ownership of a payment/ stake child private key. -Extended public keys (xpub keys) serve a dual purpose in our design: **authentication** and **efficient transaction querying**. Here's how it works: +The private key serves as **authentication** purpose in our design. Here's how it works: #### Account Authentication From 9b6bb4bfb53a9b99e8e464f4c8aa40078b52a75c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Mon, 22 Jul 2024 18:43:57 +0300 Subject: [PATCH 06/13] docs: remove docs folder --- docs/messages/client/subscribe.md | 228 ------------------------------ docs/messages/index.md | 165 --------------------- 2 files changed, 393 deletions(-) delete mode 100644 docs/messages/client/subscribe.md delete mode 100644 docs/messages/index.md diff --git a/docs/messages/client/subscribe.md b/docs/messages/client/subscribe.md deleted file mode 100644 index 19ac518..0000000 --- a/docs/messages/client/subscribe.md +++ /dev/null @@ -1,228 +0,0 @@ -# Client Subscribe Message - -Sent by client to server right after establishing a connection. New clients must authenticate themselves when subscribing to any blockchain events via a signature. - -## Schema - -```json -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "subscribe" - ] - }, - "topics": { - "type": "array", - "items": { - "type": "object", - "properties": { - "blockchain": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "network": { - "type": "string" - } - }, - "required": [ - "name", - "network" - ] - }, - "publicKey": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - }, - "signature": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - }, - /* */ - "cardano": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "properties": { - "payment": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - } - }, - "stake": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - } - } - }, - "required": [ - "payment", - "stake" - ] - }, - "config": { - "type": "object", - "properties": { - "resolveTxInput": { - "type": "boolean", - "default": false - }, - "assetMetadata": { - "type": "boolean", - "default": false - } - } - } - }, - "required": [ - "credentials", "config" - ] - } - }, - "required": [ - "blockchain", - "publicKey", - "signature" - ] - } - }, - "timestamp": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "type", - "topics", - "timestamp" - ], - "additionalProperties": false -} -``` - -The `signature` field is generated by signing a SHA256 HMAC hashed string. The prehash string is constructed by concatenating the subscribed `blockchains` + `timestamp`. - -> [!NOTE] -> For subscribing to multiple blockchains, the prehash string must be generated for each as `.`, for example: -> `cardano.mainnet+2024-06-27T12:34:56Z`. - -Apply a SHA256 HMAC using either the payment signing key or staking signing key, and then base64-encode it as final payload within the initial message. Pass the output of this SHA256 HMAC to the signature field. - -The signature verifies ownership of the private key, whose corresponding public key is used to filter relevant block transactions. - -## Subscribe Example - -```json -{ - "type": "subscribe", - "topics": [ - { - "blockchain": { "name": "cardano", "network": "mainnet" }, - "publicKey": "publicKey_example", - "signature": "OGNiOWIyNGVjOTMxZmY3N2MzYjQxOTY3OWE0YTcwMzczZmVkZmIxNDZmMDE0ODk0Nzg4YjUxMmIzMjE4MDdiYw==", // base64, SHA256 HMAC with your signing key - "cardano": { - "credentials": { - "payment": ["script...", "addr_vkh..."], // this field follows CIP-0005 - "stake": ["script...", "addr_vkh..."] // this field follows CIP-0005 - }, - "config": { - "resolveTxInput": true, - "assetMetadata": true - } - } - } - ], - "timestamp": "2024-06-27T12:34:56Z" -} -``` - -## Subscription Object - -The subscription object is blockchain specific, because there are different networks, credentials or other fields required. -Chain specific fields are grouped under a specific field named after the given chain. -In the case of Cardano blockchain, all the relevant fields are placed under a field named `cardano`. -Below we list the currently supported subscriptions: - -### Cardano - -This is the format for a Cardano specific object in the `topics` array. A client can subscribe to multiple accounts at once by providing multiple objects or one by one at any given time. - -```json -{ - "blockchain": { - "name": "cardano", - "network": { - "type": "string", - "enum": ["mainnet", "preprod", "preview"] - } - }, - "publicKey": { - "type": "string" - }, - "signature": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - }, - "cardano": { - "type": "object", - "properties": { - "credentials": { - "type": "object", - "properties": { - "payment": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - } - }, - "stake": { - "type": "array", - "items": { - "type": "string", - "pattern": "^[A-Za-z0-9+/=]*$" - } - } - }, - "required": [ - "payment", "stake" - ] - }, - "config": { - "type": "object", - "properties": { - "resolveTxInput": { - "type": "boolean", - "default": false - }, - "assetMetadata": { - "type": "boolean", - "default": false - } - } - } - }, - "required": ["credentials", "config"] - }, - "required": ["blockchain", "publicKey", "signature", "cardano"] -} -``` - -### Other blockchains - -Any other blockchain schema should follow the same structure as the above Cardano example by applying these: - -* Group blockchain specific properties under one property named after the blockchain (see `cardano` field) -* Keep chain specific authentication fields under `credentials` property -* Keep chain specific configuration fields under `config` property -* Define a JSON Schema in the documentation diff --git a/docs/messages/index.md b/docs/messages/index.md deleted file mode 100644 index 068d7ca..0000000 --- a/docs/messages/index.md +++ /dev/null @@ -1,165 +0,0 @@ -# Message Types - -This index categorizes supported message types in our event-driven communication protocol, distinguishing between client-sent and server-sent messages. It serves as the foundation for understanding the protocol's structure and functionality. - -> [!NOTE] -> We use the schema to define messages. - -## Client Messages - -- [`heartbeat`](./client/heartbeat.md): Connection management/ keep alive -- [`subscribe`](./client/subscribe.md): Define topics of interest & authentication -- [`unsubscribe`](./client/unsubscribe.md): Graceful disconnect from server - -## Server Messages - -Server-sent messages are sent either **periodically** or **by trigger** due to newly available, relevant data for a specific client. - -### Message Partials - -Different blockchains have different **periodic** events that are not related to specific transactions, but are required for either transaction construction or necessary to derive the correct wallet state. These events get represented by additional top level message keys (message partial) correlated to a chain [`point`](#event-sequencing-and-synchronization). - -We differentiate between [Ledger](#ledger-events) and [Network](#network-events) events. - -#### Ledger events - -Ledger events are specific occurrences that directly affect the state of a blockchain's ledger. These events typically involve changes to account balances, transaction histories, and other financial records. - -##### Examples - -- new transactions -- smart contract interactions -- token transfers -- stake delegation changes - -#### Network events - -Network events are occurrences that impact the overall operation and configuration of the blockchain network but do not directly alter the ledger's state. These events often involve changes to the network's consensus mechanism, protocol parameters, or infrastructure. - -##### Examples - -- Difficulty Adjustments (Bitcoin): Changes to the mining difficulty to maintain a consistent block production rate. -- Halving Events (Bitcoin): Reductions in the block reward given to miners, which occur approximately every four years. -- Epoch Transitions (Cardano): Periodic changes in the network's epoch, which can involve updates to protocol parameters and staking rewards. -- Era/ Network Transitions (Cardano): Cardano has transitioned through different [eras](https://roadmap.cardano.org/en/) which add/ remove or change certain network properties -- Network Upgrades (Ethereum): Implementation of new protocol features or improvements, such as the transition to Proof-of-Stake or gas price adjustments. -- Slot Leader Schedule Updates (Solana): Changes to the schedule of validators responsible for producing blocks. - -## Server Message Partials - -- [`genesis`](./server/genesis.md): If applicable for the subscribed blockchain, it serves static data used as part of the network bootstrap -- [`transaction`](./server/transaction.md): A new transaction -- [`tip`](./server/tip.md): A new block was appended - -### Cardano - -- [`protocol parameters`](./server/cardano/protocol-parameters.md): Epoch based message for updated protocol parameters -- [`era summary`](./server/cardano/era-summary.md): Era based message for updated slot length - -## Multichain Support - -Each server-sent message incorporates a unique chain-specific identifier. This identifier serves to precisely indicate the blockchain and network to which the message pertains. - -```json -"chain": { - "blockchain": "cardano", - "network": "mainnet", - /* ... */ -} -``` - -> [!NOTE] -> The fields of `chain` may slightly vary because some networks require additional metadata. -> For example, Ethereum-compatible chains, the `chain_id` field is particularly important as it corresponds to the [EIP-155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) chain ID.
- -A client may define multiple [`subscription`](./messages/client/subscribe.md) objects (topics of interest) to receive events from different blockchains. -This is done via subscriptions by proving authenticity depending on the blockchain. - -A client has the capability to initiate multiple subscriptions either in a single message at once or sequentially over time via a unified WebSocket connection. -When multiple subscriptions are grouped within a single message, any authentication failure in one subscription results in the intentional failure of all subscriptions included in that message. - -Alternatively, when subscriptions are executed sequentially, each subscription operation remains independent and does not impact the others. - -Furthermore, a client may opt for a dedicated WebSocket connection per subscription, restricting each WebSocket connection to handle only one subscription at a time. - -## Event Sequencing and Synchronization - -As described on a high-level before, each event server-sent message includes a `point` object. This object is crucial for tracking a client's progress in synchronizing with the current tip of a **specific** blockchain. -This type may vary depending on the blockchain, the following shows a few examples: - -## Schema - -```json -"point": { - "type": "object", - "oneOf": [ - { - "properties": { - "height": { - "type": "integer" - }, - "hash": { - "type": "string" - } - }, - "required": ["height", "hash"], - "additionalProperties": false - }, - { - "properties": { - "slot": { - "type": "integer" - }, - "hash": { - "type": "string" - } - }, - "required": ["slot", "hash"], - "additionalProperties": false - } - ] -} -``` - -#### Cardano Point - -```json -"point": { - "slot": 127838345, // absolute slot number - "hash": "9f06ab6ecce25041b3a55db6c1abe225a65d46f7ff9237a8287a78925b86d10e" // block hash -} -``` - -#### Bitcoin Point - -```json -"point": { - "height": 849561, // block height - "hash": "00000000000000000000e51f4863683fdcf1ab41eb1eb0d0ab53ee1e69df11bb" // block hash -} -``` - -#### Ethereum Point - -```json -"point": { - "height": 20175853, // block height - "hash": "0xc5ae7e8e0107fe45f7f31ddb2c0456ec92547ce288eb606ddda4aec738e3c8ec" // block hash -} -``` - -### `Point` Uses - -The `point` object serves several important functions: - -1. **Synchronization**: - - It allows clients to keep track of their current position in the blockchain, enabling them to request only new events since their last known point. More details can be found in [02-Synchronization](../02-Synchronization.md). - -2. **Consistency**: - - In case of chain reorganizations, clients can use the blockchain-specific point to detect and handle any changes in the blockchain history by handling `rollback` events properly. - -3. **Resumability**: - - If a client disconnects and reconnects, it can provide its last known point to resume event processing from where it left off for a particular blockchain. From c2da8d4425286f0ebbff0b23536f5f23fdadfff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 23 Jul 2024 12:08:59 +0300 Subject: [PATCH 07/13] docs: mark required fields as required --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 109 ++++++++++++++++++++-- 1 file changed, 102 insertions(+), 7 deletions(-) diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml index c07a595..f1093d2 100644 --- a/docs/CIP/CIP-XXXX/src/cardano-service.yml +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -190,10 +190,41 @@ components: type: array items: type: object - dditionalProperties: false properties: name: type: string + network: + type: string + extensions: + type: array + items: + type: object + properties: + name: + type: string + config: {} + default: {} + switchable: + type: boolean + versions: + type: array + items: + type: string + required: + - name + - config + - default + - switchable + - versions + version: + type: string + required: + - name + - network + - extensions + - version + required: + - blockchains genesisPayload: type: object properties: @@ -218,6 +249,17 @@ components: type: integer securityParam: type: integer + required: + - activeSlotsCoefficient + - updateQuorum + - maxLovelaceSupply + - networkMagic + - epochLength + - systemStart + - slotsPerKesPeriod + - slotLength + - maxKesEvolutions + - securityParam transactionPayload: type: object properties: @@ -244,11 +286,17 @@ components: type: integer hash: type: string + height: + type: string + required: + - hash tipPayload: type: object properties: point: $ref: '#/components/schemas/pointPayload' + required: + - point protocolParameterPayload: type: object properties: @@ -321,6 +369,38 @@ components: type: integer coinsPerUtxoSize: type: string + required: + - epoch + - minFeeA + - minFeeB + - maxBlockSize + - maxTxSize + - maxBlockHeaderSize + - keyDeposit + - poolDeposit + - eMax + - nOpt + - a0 + - rho + - tau + - decentralisationParam + - extraEntropy + - protocolMajorVer + - protocolMinorVer + - minUtxo + - minPoolCost + - nonce + - costModels + - priceMem + - priceStep + - maxTxExMem + - maxTxExSteps + - maxBlockExMem + - maxBlockExSteps + - maxValSize + - collateralPercent + - maxCollateralInputs + - coinsPerUtxoSize eraSummaryPayload: type: object properties: @@ -329,17 +409,26 @@ components: properties: epochLength: type: integer - slotLength: - type: integer - description: Milliseconds + slotLength: + type: integer + description: Milliseconds + required: + - epochLength + - slotLength start: type: object properties: slot: type: integer - time: - type: string - format: date-time + time: + type: string + format: date-time + required: + - slot + - time + required: + - parameters + - start subscribePayload: type: object properties: @@ -359,6 +448,9 @@ components: type: string network: type: string + required: + - name + - network publicKey: type: string pattern: '^[A-Za-z0-9+/=]*$' @@ -381,6 +473,9 @@ components: items: type: string pattern: '^[A-Za-z0-9+/=]*$' + required: + - payment + - stake points: type: array properties: From a42d81a28881c1ccb2df8c9007a51f42d2716415 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 23 Jul 2024 12:53:02 +0300 Subject: [PATCH 08/13] docs: reference point object in asyncapi --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml index f1093d2..88ae162 100644 --- a/docs/CIP/CIP-XXXX/src/cardano-service.yml +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -274,11 +274,13 @@ components: type: array format: string point: - type: string + type: object + $ref: '#/components/schemas/pointPayload' chain: type: string tip: - type: string + type: object + $ref: '#/components/schemas/pointPayload' pointPayload: type: object properties: @@ -478,6 +480,9 @@ components: - stake points: type: array + items: + type: object + $ref: '#/components/schemas/pointPayload' properties: slot: type: integer From f0972a04724e78c0fe568885ec7eb4b83462b55c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 23 Jul 2024 13:39:52 +0300 Subject: [PATCH 09/13] docs: change server urls to localhost --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml index 88ae162..6465983 100644 --- a/docs/CIP/CIP-XXXX/src/cardano-service.yml +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -20,7 +20,7 @@ info: defaultContentType: application/json servers: cardano-mainnet: - host: mainnet.lacev2.com + host: 'localhost:8080' protocol: wss description: Cardano Mainnet API tags: @@ -29,11 +29,11 @@ servers: - name: 'visibility:public' description: This resource is public to all users cardano-preprod: - host: preprod.lacev2.com + host: 'localhost:8080' protocol: wss description: Cardano Preprod API tags: - - name: 'env:cardano-mainnet' + - name: 'env:cardano-preprod' description: This environment is for Cardano preprod - name: 'visibility:public' description: This resource is public to all users From c44b90a05d8ba855debd6baaf644ffefad4d481e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Tue, 23 Jul 2024 14:42:38 +0300 Subject: [PATCH 10/13] docs: add examples and payloads --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 138 +++++++++++++++++++++- 1 file changed, 137 insertions(+), 1 deletion(-) diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml index 6465983..cf2f3c0 100644 --- a/docs/CIP/CIP-XXXX/src/cardano-service.yml +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -116,6 +116,32 @@ components: the server and API, helping clients understand the environment they are interacting with. contentType: application/json + payload: + $ref: '#/components/schemas/welcomePayload' + examples: + - + name: Welcome + summary: Welcome message example + payload: + blockchains: + - name: cardano + network: mainnet + extensions: + - name: assetMetadata + config: false + switchable: true + versions: + - '1.0' + - '1.1' + - name: CIP-XXXX + config: + timeout: 10 + switchable: false + versions: + - '1.0' + uri: wss://www.example.com/socketserver + version: '0.1' + genesis: name: Genesis title: Genesis message @@ -148,6 +174,48 @@ components: contentType: application/json payload: $ref: '#/components/schemas/protocolParameterPayload' + examples: + - + name: protocolParameters + summary: Protocol Parameters message example + payload: + epoch: 225 + minFeeA: 44 + minFeeB: 155381 + maxBlockSize: 65536 + maxTxSize: 16384 + maxBlockHeaderSize: 1100 + keyDeposit: '2000000' + poolDeposit: '500000000' + eMax: 18 + nOpt: 150 + a0: 0.3 + rho: 0.003 + tau: 0.2 + decentralisationParam: 0.5 + extraEntropy: + protocolMajorVer: 2 + protocolMinorVer: 0 + minUtxo: '1000000' + minPoolCost: '340000000' + nonce: 1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81 + costModels: + plutusV1: + addIntegerCpuArgumentsIntercept: 197209 + addIntegerCpuArgumentsSlope: 0 + plutusV2: + addIntegerCpuArgumentsIntercept: 197209 + addIntegerCpuArgumentsSlope: 0 + priceMem: 0.0577 + priceStep: 7.21e-05 + maxTxExMem: '10000000' + maxTxExSteps: '10000000000' + maxBlockExMem: '50000000' + maxBlockExSteps: '40000000000' + maxValSize: '5000' + collateralPercent: 150 + maxCollateralInputs: 3 + coinsPerUtxoSize: '34482' tip: name: Tip title: Server Tip Message Partial @@ -155,7 +223,25 @@ components: This message partial is added to all server-sent messages and represents the latest current tip of the chain. payload: - $ref: '#/components/schemas/protocolParameterPayload' + $ref: '#/components/schemas/tipPayload' + examples: + - + name: Server Tip message + summary: Cardano Example + payload: + tip: + point: + slot: 127838345 + hash: 9f06ab6ecce25041b3a55db6c1abe225a65d46f7ff9237a8287a78925b86d10e + - + name: Server Tip message + summary: Bitcnoin Example + payload: + tip: + point: + height: 849561 + hash: 00000000000000000000e51f4863683fdcf1ab41eb1eb0d0ab53ee1e69df11bb + eraSummary: name: Era Summary title: Era Summary Server Message Partial @@ -172,6 +258,25 @@ components: slot length, the conversion of a specific point in on-chain time may vary. In order to show and submit transaction times correctly, wallets need to know each era's slot length. + payload: + $ref: '#/components/schemas/eraSummaryPayload' + examples: + - + name: Era Summary Message + summary: Era Summary Message example + payload: + parameters: + epochLength: 432000 + slotLength: 1 + safeZone: 129600 + start: + epoch: 74 + slot: 1598400 + time: '2020-09-11T08:36:51.000Z' + end: + epoch: 102 + slot: 13694400 + time: '2020-12-30T05:04:51.000Z' subscribe: name: Subscribe title: Subscribe message @@ -182,6 +287,37 @@ components: contentType: application/json payload: $ref: '#/components/schemas/subscribePayload' + examples: + - + name: Subscribe + summary: Subscribe message + payload: + type: subscribe + topic: + blockchain: + name: cardano + network: mainnet + signature: OGNiOWIyNGVjOTMxZmY3N2MzYjQxOTY3OWE0YTcwMzczZmVkZmIxNDZmMDE0ODk0Nzg4YjUxMmIzMjE4MDdiYw== + cardano: + credentials: + payment: + - 0d166978f407505f157c9f56fdb3358e60d1589295b2f7a1e66c1574 + stake: + - 82c00414a674fd7e7657aa5634e2086910c2f210e87f22ce880a0063 + points: + - slot: 66268628 + hash: 47b8ec3a58a4a69cb5e3397c36cb3966913882fa8179cae10a5d3f9319c4ae66 + - slot: 87868775 + hash: '074985b22edc01b9579a2e571dc125e044aecf812ee45d50e6fb6fef979fd0d0' + extensions: + - name: resolveTxInput + config: true + version: '1.0' + - name: assetMetadata + config: false + version: '1.0' + timestamp: '2024-06-27T12:34:56Z' + schemas: welcomePayload: type: object From b765bfda5ca30b96082884fd9537235898fe0321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Wed, 24 Jul 2024 10:50:11 +0300 Subject: [PATCH 11/13] docs: update AsyncApi servers section --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 26 +++++++++-------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml index cf2f3c0..25b84c0 100644 --- a/docs/CIP/CIP-XXXX/src/cardano-service.yml +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -19,24 +19,18 @@ info: url: 'https://www.apache.org/licenses/LICENSE-2.0' defaultContentType: application/json servers: - cardano-mainnet: - host: 'localhost:8080' + local-server: + host: '127.0.0.1:{port}' protocol: wss - description: Cardano Mainnet API + description: Default instance, when running a local server. + variables: + port: + default: '8080' tags: - - name: 'env:cardano-mainnet' - description: This environment is for Cardano mainnet production - - name: 'visibility:public' - description: This resource is public to all users - cardano-preprod: - host: 'localhost:8080' - protocol: wss - description: Cardano Preprod API - tags: - - name: 'env:cardano-preprod' - description: This environment is for Cardano preprod - - name: 'visibility:public' - description: This resource is public to all users + - name: 'env:local' + description: This environment is for local + - name: 'visibility:private' + description: This resource is locally visible channels: cardano: address: /v1/wallet From b206bf16e008c80cda477fe58fec40e6175b84f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C4=B1z=C4=B1r=20Sefa=20=C4=B0rken?= Date: Wed, 24 Jul 2024 12:02:16 +0300 Subject: [PATCH 12/13] docs: update onGenesis summary --- docs/CIP/CIP-XXXX/src/cardano-service.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CIP/CIP-XXXX/src/cardano-service.yml b/docs/CIP/CIP-XXXX/src/cardano-service.yml index 25b84c0..effd153 100644 --- a/docs/CIP/CIP-XXXX/src/cardano-service.yml +++ b/docs/CIP/CIP-XXXX/src/cardano-service.yml @@ -68,7 +68,7 @@ operations: channel: $ref: '#/channels/cardano' onGenesis: - summary: On client connection + summary: On client syncing the genesis description: Event received if the client is syncing from the origin action: receive messages: From 42c81aa5be4abb9a230e654502c216ce13ee2e5b Mon Sep 17 00:00:00 2001 From: William <9065638+will-break-it@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:29:47 +0200 Subject: [PATCH 13/13] Apply suggestions from code review Co-authored-by: Rhys Bartels-Waller --- docs/CIP/CIP-XXXX/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/CIP/CIP-XXXX/README.md b/docs/CIP/CIP-XXXX/README.md index a34c9ba..6355d9f 100644 --- a/docs/CIP/CIP-XXXX/README.md +++ b/docs/CIP/CIP-XXXX/README.md @@ -53,7 +53,7 @@ We distinguish between client-sent and server-sent messages. Client-sent messages are **typed** messages, such as: -```json +```json5 { "type": "" // ... @@ -134,7 +134,7 @@ The credentials also enable efficient querying of client-relevant data: 2. **BIP32 Wallet Query Process** - The server queries our index using the corresponding key hashes per client to serve only relevant transactions to scubribed clients. + The server queries our index using the corresponding key hashes per client to serve only relevant transactions to subscribed clients. ### Ordering of Events