12.46. DD 46: Contract Format v1¶
12.46.1. Summary¶
The contract v1 format enables a multitude of advanced interactions between merchants and wallets, including donations, subscriptions, coupons, currency exchange and more.
12.46.2. Motivation¶
The existing v0 contract format is too simplistic to support many frequenly requested types of contracts.
12.46.3. Requirements¶
We want Taler to support various interesting use-cases:
Unlinkable, uncopyable subscriptions without accounts (reader can pay with Taler to subscribe to online publication, read unlimited number of articles during a certain period, transfer subscription to other devices, maintain unlinkability / full anonymity amongst all anonymous subscribers).
Coupons, discounts and stamps – like receiving a discount on a product, product basket or subscription – based on previous purchase(s). Again, with unlinkability and anonymity (modulo there being other users eligible for the discount).
Subscription tokens lost (due to loss of device without backup) should be recoverable from any previous backup of the subscription.
Currency conversion, that is exchanging one currency for another.
Donations, including privacy-preserving tax receipts that prove that the user donated to an entity that is eligible for tax-deductions but without revealing which entity the user donated to. At the same time, the entity issuing the tax receipt must be transparent (to the state) with respect to the amount of tax-deductable donations it has received.
Throttled political donations where each individual is only allowed to donate anonymously up to a certain amount per year or election cycle.
Unlinkable gifts – enabling the purchase of digital goods (such as articles, albums, etc.) to be consumed by a third party. For example, a newspaper subscription may include a fixed number of articles that can be gifted to others each week, all while maintaining unlinkability and anonymity between the giver and the recipient.
Temporally-constrained, unlinkable event ticketing. Allowing visitors to use Taler to purchase a ticket for an event. This ticket grants entry and exit privileges to the event location during a specified time window, while preserving the anonymity of the ticket holder (within the group of all the ticket holders).
Event deposit systems. A deposit mechanism for events where customers receive a token alongside their cup or plate, which they are expected to return. This system validates that the cup or plate was legitimately acquired (i.e., not brought from home or stolen from a stack of dirty items) and incentivizes return after use.
12.46.4. Proposed Solution¶
Merchants will also blindly sign tokens (not coins) to indicate the eligibility of a user for certain special offers. Contracts will be modified to allow requiring multiple inputs (to be provisioned to the merchant) and multiple outputs (to be yielded by the merchant). The wallet will then allow the user to select between the choices that the user could pay for, or possibly make an automatic choice if the correct choice is obvious. One output option is blindly signed coins from another exchange, possibly in a different currency. Another output option is blindly signed donation receipts from a DONation AUthority (DONAU). Subscriptions can be modeled by requiring the wallet to provision a token of the same type that is also yielded by the contract. For security, payments using subscription tokens (and possibly certain other special tokens?) will be limited to a list of domains explicitly defined as trusted by the token issuer. When paying for a contract, the wallet must additionally sign over the selected sub-contract index and a hash committing it to the blinded envelopes (if any). The merchant backend will (probably?) need to be changed to truly support multiple currencies (ugh).
12.46.4.1. New Contract Terms Format¶
The contract terms v1 will have the following structure:
interface ContractTermsV1 {
// This is version 1, the previous contract terms SHOULD
// be indicated using "0", but in v0 specifying the version
// is optional.
version: 1;
// Unique, free-form identifier for the proposal.
// Must be unique within a merchant instance.
// For merchants that do not store proposals in their DB
// before the customer paid for them, the order_id can be used
// by the frontend to restore a proposal from the information
// encoded in it (such as a short product identifier and timestamp).
order_id: string;
// URL where the same contract could be ordered again (if
// available). Returned also at the public order endpoint
// for people other than the actual buyer (hence public,
// in case order IDs are guessable).
public_reorder_url?: string;
// Time when this contract was generated.
timestamp: Timestamp;
// After this deadline, the merchant won't accept payments for the contract.
pay_deadline: Timestamp;
// Transfer deadline for the exchange. Must be in the
// deposit permissions of coins used to pay for this order.
wire_transfer_deadline: Timestamp;
// Merchant's public key used to sign this proposal; this information
// is typically added by the backend. Note that this can be an ephemeral key.
merchant_pub: EddsaPublicKey;
// Base URL of the (public!) merchant backend API.
// Must be an absolute URL that ends with a slash.
merchant_base_url: string;
// More info about the merchant (same as in v0).
merchant: Merchant;
// Human-readable description of the contract.
summary: string;
// Map from IETF BCP 47 language tags to localized summaries.
summary_i18n?: { [lang_tag: string]: string };
// URL that will show that the order was successful after
// it has been paid for. Optional. When POSTing to the
// merchant, the placeholder "${ORDER_ID}" will be
// replaced with the actual order ID (useful if the
// order ID is generated server-side and needs to be
// in the URL).
// Note that this placeholder can only be used once.
// Either fulfillment_url or fulfillment_message must be specified.
fulfillment_url?: string;
// Message shown to the customer after paying for the order.
// Either fulfillment_url or fulfillment_message must be specified.
fulfillment_message?: string;
// Map from IETF BCP 47 language tags to localized fulfillment
// messages.
fulfillment_message_i18n?: { [lang_tag: string]: string };
// List of products that are part of the purchase (see Product).
products: Product[];
// After this deadline has passed, no refunds will be accepted.
refund_deadline: Timestamp;
// Specifies for how long the wallet should try to get an
// automatic refund for the purchase. If this field is
// present, the wallet should wait for a few seconds after
// the purchase and then automatically attempt to obtain
// a refund. The wallet should probe until "delay"
// after the payment was successful (i.e. via long polling
// or via explicit requests with exponential back-off).
//
// In particular, if the wallet is offline
// at that time, it MUST repeat the request until it gets
// one response from the merchant after the delay has expired.
// If the refund is granted, the wallet MUST automatically
// recover the payment. This is used in case a merchant
// knows that it might be unable to satisfy the contract and
// desires for the wallet to attempt to get the refund without any
// customer interaction. Note that it is NOT an error if the
// merchant does not grant a refund.
auto_refund?: RelativeTime;
// Delivery location for (all!) products (same as in v0).
delivery_location?: Location;
// Time indicating when the order should be delivered.
// May be overwritten by individual products.
delivery_date?: Timestamp;
// Nonce generated by the wallet and echoed by the merchant
// in this field when the proposal is generated.
// Note: required in contract, absent in order!
nonce: string;
// Array of possible specific contracts the wallet/customer
// may choose from by selecting the respective index when
// signing the deposit confirmation.
choices: ContractChoice[];
// Map from token family slugs to meta data about the
// respective token family.
token_families: { [token_family_slug: string]: ContractTokenFamily };
// Extra data that is only interpreted by the merchant frontend.
// Useful when the merchant needs to store extra information on a
// contract without storing it separately in their database.
extra?: any;
// Exchanges that the merchant accepts for this currency.
exchanges: Exchange[];
}
interface ContractChoice {
// Price to be paid for this choice. Could be 0.
// The price is in addition to other instruments,
// such as rations and tokens.
// The exchange will subtract deposit fees from that amount
// before transferring it to the merchant.
amount: Amount;
// List of inputs the wallet must provision (all of them) to
// satisfy the conditions for the contract.
inputs: ContractInput[];
// List of outputs the merchant promises to yield (all of them)
// once the contract is paid.
outputs: ContractOutput[];
// Maximum total deposit fee accepted by the merchant for this contract.
max_fee: Amount;
}
type ContractInput =
| ContractInputRation
| ContractInputToken;
interface ContractInputRation {
type: "coin";
// Price to be paid for the transaction.
price: Amount;
// FIXME-DOLD: do we want to move this into a 'details'
// sub-structure as done with tokens below?
class: "ration";
// Base URL of the ration authority.
ration_authority_url: string;
};
interface ContractInputToken {
type: "token";
// Slug of the token family in the
// 'token_families' map on the order.
token_family_slug: string;
// Start of the validity period of the token. This is used to find the
// matching public key within the token family.
valid_after: Timestamp;
// Number of tokens of this type required.
// Defaults to one if the field is not provided.
number?: Integer;
};
type ContractOutput =
| ContractOutputCoin
| ContractOutputTaxReceipt
| ContractOutputToken;
interface ContractOutputCoin {
type: "coins";
// Amount of coins that will be yielded.
// This excludes any applicable withdraw fees.
brutto_yield: Amount;
// Base URL of the exchange that will issue the
// coins.
exchange_url: string;
};
interface ContractOutputTaxReceipt {
type: "tax-receipt";
// Base URL of the donation authority that will
// issue the tax receipt.
donau_url: string;
};
interface ContractOutputToken {
type: "token";
// Slug of the token family in the
// 'token_families' map on the top-level.
token_family_slug: string;
// Start of the validity period of the token. This is used to find the
// matching public key within the token family.
valid_after: Timestamp;
// Number of tokens to be issued.
// Defaults to one if the field is not provided.
number?: Integer;
}
type ContractTokenDetails =
| ContractSubscriptionTokenDetails
| ContractDiscountTokenDetails
interface ContractSubscriptionTokenDetails {
class: "subscription";
// Array of domain names where this subscription
// can be safely used (e.g. the issuer warrants that
// these sites will re-issue tokens of this type
// if the respective contract says so). May contain
// "*" for any domain or subdomain.
trusted_domains: string[];
};
interface ContractDiscountTokenDetails {
class: "discount";
// Array of domain names where this discount token
// is intended to be used. May contain "*" for any
// domain or subdomain. Users should be warned about
// sites proposing to consume discount tokens of this
// type that are not in this list that the merchant
// is accepting a coupon from a competitor and thus
// may be attaching different semantics (like get 20%
// discount for my competitors 30% discount token).
expected_domains: string[];
};
interface ContractTokenFamily {
// Human-readable name of the token family.
name: string;
// Human-readable description of the semantics of
// this token family (for display).
description: string;
// Map from IETF BCP 47 language tags to localized descriptions.
description_i18n?: { [lang_tag: string]: string };
// Public keys used to validate tokens issued by this token family.
keys: TokenIssuePublicKey[];
// Class-specific information of the token
details: ContractTokenDetails;
// Must a wallet understand this token type to
// process contracts that consume or yield it?
critical: boolean;
// Number of tokens issued according to ASS authority
// FIXME: this is still rather speculative in the design...
ass?: Integer;
// Signature affirming sum of token issuance deposit (?) fees
// collected by an exchange according to the ASS authority.
// FIXME: this is still rather speculative in the design...
ass_cost?: Amount;
// Signature affirming the ass by the ASS authority.
// FIXME: this is still rather speculative in the design...
ass_sig?: EddsaSignature;
};
type TokenIssuePublicKey =
| TokenIssueRsaPublicKey
| TokenIssueCsPublicKey;
interface TokenIssueRsaPublicKey {
cipher: "RSA";
// RSA public key.
rsa_pub: RsaPublicKey;
// Start time of this key's validity period.
valid_after: Timestamp;
// End time of this key's validity period.
valid_before: Timestamp;
}
interface TokenIssueCsPublicKey {
cipher: "CS";
// CS public key.
cs_pub: Cs25519Point;
// Start time of this key's validity period.
valid_after: Timestamp;
// End time of this key's validity period.
valid_before: Timestamp;
}
12.46.4.2. Alternative Contracts¶
The contract terms object may contain any number of alternative contracts that the user must choose between. The alternatives can differ by inputs, outputs or other details. The wallet must filter the contracts by those that the user can actually pay for, and move those that the user could currently not pay for to the end of the rendered list. Similarly, the wallet must move up the cheaper contracts, so if a contract has a definitively lower price and consumes an available discount token, that contract should be moved up in the list.
Which specific alternative contract was chosen by the user is indicated in the
choice_index
field of the TALER_DepositRequestPS.
12.46.4.3. Output Commitments¶
When a contract has outputs, the wallet must send an array of blinded tokens, coins or tax receipts together with the payment request. The order in the array must match the order in the outputs field of the contract. For currency outputs, one array element must include all of the required planchets for a batch withdrawal, but of course not the reserve signature.
Note
We can probably spec this rather nicely if we first change the batch-withdraw API to only use a single reserve signature.
This array of blinded values is hashed to create the output commitment hash
(h_outputs
) in the TALER_DepositRequestPS.
12.46.4.4. Subscriptions¶
The user buys a subscription (and possibly at the same time an article) using currency and the contract yields an additional subscription token as an output. Active subscriptions are listed below the currencies in the wallet under a new heading. Subscriptions are never auto-renewing, if the user wants to extend the subscription they can trivially pay for it with one click.
When a contract consumes and yields exactly one subscription token of the same type in a trusted domain, the wallet may automatically approve the transaction without asking the user for confirmation (as it is free).
The token expiration for a subscription can be past the “end date” to enable a previous subscription to be used to get a discount on renewing the subscription. The wallet should show applicable contracts with a lower price that only additionally consume subscription tokens after their end date before higher-priced alternative offers.
Subscription tokens are “critical” in that a wallet implementation must understand them before allowing a user to interact with this class of token. Subscription token secrets should be derived from a master secret associated with the subscription, so that the private keys are recoverable from backup. To obtain the blind signatures, a merchant must offer an endpoint where one can submit the public key of the N-1 subscription token and obtain the blinded signature over the N-th subscription token. The wallet can then effectively recover the subscription from backup using a binary search.
The merchant SPA should allow the administrator to create (maybe update) and delete subscriptions. Each subscription is identified by a subscription label and includes a validity period.
The merchant backend must then automatically manage (create, use, delete) the respective signing keys. When creating an order, the frontend can just refer to the subscription label (and possibly a start date) in the inputs or outputs. The backend should then automatically substitute this with the respective cryptographic fields for the respective time period and subscription label.
12.46.4.5. Discounts¶
To offer a discount based on one or more previous purchases, a merchant must yield some discount-specific token as an output with the previous purchase, and then offer an alternative contract with a lower price that consumes currency and the discount token. The wallet should show contracts with a lower price that only additionally consume discount tokens
The merchant SPA should allow the administrator to create (maybe update) and delete discount tokens. Each discount token is identified by a discount label and includes an expiration time or validity duration.
The merchant backend must then automatically manage (create, use, delete) the respective signing keys. When creating an order, the frontend can just refer to the discount token label in the inputs or outputs. The backend should then automatically substitute this with the respective cryptographic fields for the respective discount token.
12.46.4.7. Tax Receipts¶
Tax receipts differ from coins and tokens in that what is blindly signed over
should be the taxpayer identification number of the tax payer. The format of
the taxpayer identification number should simply be a string, with the rest
being defined by the national authority. The DONAU should indicate in its
/config
response what format this string should have, using possibly both
an Anastasis-style regex and an Anastasis-style function name (to check things
like checksums that cannot be validated using a regex). Wallets must then
validate the regex (if given) and if possible should implement the
Anastasis-style logic.
Wallets should collect tax receipts by year and offer an export functionality. The export should generate either
a JSON file,
a PDF (with QR codes), or
a series of screens with QR codes.
Wallets may only implement some of the above options due to resource constraints.
The documents should encode the taxpayer ID, the amount and the DONAU signature (including the year, excluding the exact public key as there should only be one possible).
12.46.4.8. Rationing (future work)¶
If per-capita rationing must be imposed on certain transactions, a rationing authority (RA) must exist that identifies each eligible human and issues that human a number of ration coins for the respective rationing period. An RA largely functions like a regular exchange, except that eligible humans will need to authenticate directly to withdraw rations (instead of transferring fiat to an exchange). Merchants selling rationed goods will be (legally) required to collect deposit confirmations in proportion to the amount of rationed goods sold. A difference to regular exchanges is that RAs do not charge any fees. RAs may or may not allow refreshing rations that are about to expire for ration coins in the next period.
Once an RA is added to a wallet, it should automatically try to withdraw the maximum amount of ration coins it is eligible for. Available rations should be shown below the subscriptions by RA (if any).
..note:
RAs are considered an idea for future work and not part of our current timeline.
12.46.4.9. Limited Donations per Capita (future work)¶
If per-capita limitations must be imposed on anonymous donations (for example for donations to political parties), an RA can be used to issue donation rations that limit the amount of donations that can be made for the respective period.
..note:
RAs are considered an idea for future work and not part of our current timeline.
12.46.5. Definition of Done¶
Merchant backend support for multiple currencies
Merchant backend support for consuming and issuing tokens
Merchant SPA support for configuring new tokens of different types
Wallet-core support for various new contract types
Wallet-core filters for feasible contracts and possibly auto-executes subscriptions
Wallet-GUIs (WebEx, Android, iOS) render new contract types
Wallet-GUIs (WebEx, Android, iOS) allow user to select between multiple contracts
Documentation for developers is up-to-date
Token anonymity set size (ASS) authority implemented, documented
Merchants report anonymity set size increases to ASS authority
Wallets process anonymity set size reports from ASS authority
Bachelor thesis written on applications and design
Academic paper written on DONAU (requirements, design, implementation)
DONAU implemented, documented
DONAU receipt validation application implemented
Integration tests exist in wallet-core
Deliverables accepted by EC
While rationing is part of the design, we expect the actual implementation to be done much later and thus should not consider it part of the “DONE” part. Rationing is complex, especially as a refunded contract should probably also refund the ration.
12.46.6. Alternatives¶
The first draft of this DD included the capability of paying with multiple currencies for the same contract (for example, USD:1 and EUR:5) plus tokens and rations. However, this is very complex, both for wallets (how to display), for other merchant APIs (does the refund API have to become multi-currency as well?) and there does not seem to be a good business case for it. So for now, the price is always only in one currency.
12.46.7. Drawbacks¶
Significant change, but actually good ratio compared to use-cases covered.
12.46.8. Discussion / Q&A¶
(This should be filled in with results from discussions on mailing lists / personal communication.)