From 7214412a05e38b8b957257c3ee99d8acb6571179 Mon Sep 17 00:00:00 2001 From: Christian Grothoff Date: Thu, 6 May 2021 15:33:52 +0200 Subject: work on w2w spec --- core/api-common.rst | 27 ++ core/api-exchange.rst | 403 ++++++++++++++++++++----- design-documents/013-peer-to-peer-payments.rst | 102 ++++++- 3 files changed, 455 insertions(+), 77 deletions(-) diff --git a/core/api-common.rst b/core/api-common.rst index bb922cc2..5c05db0c 100644 --- a/core/api-common.rst +++ b/core/api-common.rst @@ -1033,6 +1033,30 @@ within the +.. _TALER_ReserveStatusRequestSignaturePS: +.. sourcecode:: c + + struct TALER_PurseStatusRequestSignaturePS { + /** + * purpose.purpose = TALER_SIGNATURE_RESERVE_STATUS_REQUEST + */ + struct GNUNET_CRYPTO_EccSignaturePurpose purpose; + }; + + +.. _TALER_ReserveHistoryRequestSignaturePS: +.. sourcecode:: c + + struct TALER_PurseStatusRequestSignaturePS { + /** + * purpose.purpose = TALER_SIGNATURE_RESERVE_HISTORY_REQUEST + */ + struct GNUNET_CRYPTO_EccSignaturePurpose purpose; + struct TALER_AmountNBO history_fee; + struct GNUNET_TIME_AbsoluteNBO request_timestamp; + }; + + .. _TALER_PurseStatusRequestSignaturePS: .. sourcecode:: c @@ -1115,6 +1139,7 @@ within the struct TALER_ReservePublicKey reserve_pub; struct GNUNET_TIME_AbsoluteNBO merge_timestamp; struct GNUNET_TIME_AbsoluteNBO purse_expiration; + struct TALER_AmountNBO purse_value_after_fees; struct GNUNET_HashCode h_contract_terms; struct GNUNET_HashCode h_wire; }; @@ -1131,6 +1156,7 @@ within the struct TALER_PursePublicKey purse_pub; struct GNUNET_TIME_AbsoluteNBO merge_timestamp; struct GNUNET_TIME_AbsoluteNBO purse_expiration; + struct TALER_AmountNBO purse_value_after_fees; struct GNUNET_HashCode h_contract_terms; struct GNUNET_HashCode h_wire; }; @@ -1176,6 +1202,7 @@ within the */ struct GNUNET_CRYPTO_EccSignaturePurpose purpose; struct GNUNET_TIME_AbsoluteNBO kyc_timestamp; + struct TALER_AmountNBO kyc_fee; struct GNUNET_HashCode h_wire; }; diff --git a/core/api-exchange.rst b/core/api-exchange.rst index 654ce31e..27886c24 100644 --- a/core/api-exchange.rst +++ b/core/api-exchange.rst @@ -978,14 +978,12 @@ exchange. advertise those terms of service. -.. http:get:: /reserves/$RESERVE_PUB +.. http:post:: /reserves/$RESERVE_PUB/status Request information about a reserve or an account. **Request:** - *Account-Request-Signature*: The client must provide Base-32 encoded EdDSA signature made with ``$ACCOUNT_PRIV``, affirming its authorization to access the account status. The purpose used MUST be ``TALER_SIGNATURE_ACCOUNT_STATUS`` (NUMBER: TBD). - :query history=BOOLEAN: *Optional.* If specified, the exchange will return the recent account history. This is still free of charge. @@ -993,6 +991,8 @@ exchange. the exchange will return the full account history. This may incur a fee that will be charged to the account. + The request body must be a `ReserveStatusRequest` object. + **Response:** :http:statuscode:`200 OK`: @@ -1005,6 +1005,14 @@ exchange. **Details:** + .. ts:def:: ReserveStatusRequest + + interface ReserveStatusRequest { + // Signature of type + // TALER_SIGNATURE_RESERVE_STATUS_REQUEST + reserve_sig: EddsaSignature; + } + .. ts:def:: ReserveStatus interface ReserveStatus { @@ -1020,6 +1028,7 @@ exchange. kyc_required: boolean; // Transaction history for this reserve. + // May be partial (!). history: TransactionHistoryItem[]; } @@ -1030,29 +1039,84 @@ exchange. // Union discriminated by the "type" field. type TransactionHistoryItem = | AccountMergeTransaction + | AccountSetupTransaction + | ReserveHistoryTransaction | ReserveWithdrawTransaction | ReserveCreditTransaction | ReserveClosingTransaction | ReserveRecoupTransaction; + .. ts:def:: AccountHistoryTransaction + + interface AccountHistoryTransaction { + type: "HISTORY"; + + // Fee agreed to by the reserve owner. + history_fee: Amount; + + // Time when the request was made. + request_timestamp: Timestamp; + + // Signature created with the reserve's private key. + // Must be of purpose TALER_SIGNATURE_RESERVE_HISTORY_REQUEST. + reserve_sig: EddsaSignature; + + } + + .. ts:def:: AccountSetupTransaction + + interface AccountSetupTransaction { + type: "SETUP"; + + // KYC fee agreed to by the reserve owner. + kyc_fee: Amount; + + // Time when the KYC was triggered. + kyc_timestamp: Timestamp; + + // Hash of the wire details of the account. + // Note that this hash is unsalted and potentially + // private (as it could be inverted), hence access + // to this endpoint must be authorized using the + // private key of the reserve. + h_wire: HashCode; + + // Signature created with the reserve's private key. + // Must be of purpose TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST. + reserve_sig: EddsaSignature; + + } + .. ts:def:: AccountMergeTransaction interface AccountMergeTransaction { type: "MERGE"; - // Amount merged (what was left after fees). + // Actual amount merged (what was left after fees). amount: Amount; + // Minimum amount merged (amount signed by the + // reserve and purse signatures). + minimum_amount: Amount; + // Purse that was merged. purse_pub: EddsaPublicKey; + // Time of the merge. + merge_timestamp: Timestamp; + + // Expiration time of the purse. + purse_expiration: Timestamp; + // Hash of the contract. h_contract: HashCode; - // Signature created with the account's private key. - account_sig: EddsaSignature; + // Signature created with the reserve's private key. + // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE. + reserve_sig: EddsaSignature; // Signature created with the purse's private key. + // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. purse_sig: EddsaSignature; // Deposit fees that were charged to the purse. @@ -1155,6 +1219,46 @@ exchange. coin_pub: CoinPublicKey; } + +.. http:post:: /reserves/$RESERVE_PUB/history + + Request information about the full history of + a reserve or an account. + + **Request:** + + The request body must be a `ReserveHistoryRequest` object. + + **Response:** + + :http:statuscode:`200 OK`: + The exchange responds with a `ReserveStatus` object; the reserve was known to the exchange. + :http:statuscode:`401 Unauthorized`: + The *Account-Request-Signature* is invalid. + This response comes with a standard `ErrorDetail` response. + :http:statuscode:`404 Not found`: + The reserve key does not belong to a reserve known to the exchange. + :http:statuscode:`412 Precondition failed`: + The balance in the reserve is insufficient to pay for the history request. + This response comes with a standard `ErrorDetail` response. + + **Details:** + + .. ts:def:: ReserveHistoryRequest + + interface ReserveHistoryRequest { + // Signature of type + // TALER_SIGNATURE_RESERVE_HISTORY_REQUEST + reserve_sig: EddsaSignature; + + // Time when the client made the request. + // Timestamp must be reasonably close to the time of + // the exchange, otherwise the exchange may reject + // the request. + request_timestamp: Timestamp; + } + + .. http:post:: /reserves/$RESERVE_PUB/withdraw Withdraw a coin of the specified denomination. Note that the client should @@ -1450,101 +1554,244 @@ denomination. .. ts:def:: CoinSpendHistoryItem - interface CoinSpendHistoryItem { - // Either "DEPOSIT", "MELT", "REFUND", "RECOUP", - // "OLD-COIN-RECOUP" or "RECOUP-REFRESH". - type: string; + // Union discriminated by the "type" field. + type CoinSpendHistoryItem = + | CoinDepositTransaction + | CoinMeltTransaction + | CoinRefundTransaction + | CoinRecoupTransaction + | CoinOldCoinRecoupTransaction + | CoinRecoupRefreshTransaction + | CoinPursePaymentTransaction; + + + .. ts:def:: CoinDepositTransaction + + interface CoinDepositTransaction { + type: "DEPOSIT"; // The total amount of the coin's value absorbed (or restored in the // case of a refund) by this transaction. - // Note that for deposit and melt this means the amount given includes - // the transaction fee, while for refunds the amount given excludes - // the transaction fee. The current coin value can thus be computed by - // subtracting deposit and melt amounts and adding refund amounts from - // the coin's denomination value. + // The amount given includes + // the deposit fee. The current coin value can thus be computed by + // subtracting this amount. amount: Amount; - // Deposit fee in case of type "DEPOSIT". + // Deposit fee. deposit_fee: Amount; - // Public key of the merchant, for "DEPOSIT" operations. - merchant_pub?: EddsaPublicKey; + // Public key of the merchant. + merchant_pub: EddsaPublicKey; // Date when the operation was made. - // Only for "DEPOSIT", "RECOUP", "OLD-COIN-RECOUP" and - // "RECOUP-REFRESH" operations. - timestamp?: Timestamp; + timestamp: Timestamp; // Date until which the merchant can issue a refund to the customer via the - // exchange, possibly zero if refunds are not allowed. Only for "DEPOSIT" operations. - refund_deadline?: Timestamp; + // exchange, possibly zero if refunds are not allowed. + refund_deadline: Timestamp; - // Signature by the coin, only present if ``type`` is "DEPOSIT", "MELT", "RECOUP", - // or "RECOUP-REFRESH". - coin_sig?: EddsaSignature; + // Signature by the coin. + coin_sig: EddsaSignature; - // Deposit fee in case of type "MELT". - melt_fee: Amount; + // Hash of the bank account from where we received the funds. + h_wire: HashCode; - // Commitment from the melt operation, only for "MELT". - rc?: TALER_RefreshCommitmentP; + // Hash of the public denomination key used to sign the coin. + // FIXME: why do we care to have this? + h_denom_pub: HashCode; + + // Hash over the proposal data of the contract that + // is being paid. + h_contract_terms: HashCode; + + } + + .. ts:def:: CoinMeltTransaction + + interface CoinMeltTransaction { + type: "MELT"; + + // The total amount of the coin's value absorbed by this transaction. + // Note that for melt this means the amount given includes + // the melt fee. The current coin value can thus be computed by + // subtracting the amounts. + amount: Amount; + + // Signature by the coin. + coin_sig: EddsaSignature; + + // Melt fee. + melt_fee: Amount; - // Hash of the bank account from where we received the funds, - // only present if ``type`` is "DEPOSIT". - h_wire?: HashCode; + // Commitment from the melt operation. + rc: TALER_RefreshCommitmentP; // Hash of the public denomination key used to sign the coin. - // only present if ``type`` is "DEPOSIT", "RECOUP", - // "RECOUP-REFRESH", or "MELT". - h_denom_pub?: HashCode; + // FIXME: why do we care to have this? + h_denom_pub: HashCode; - // Deposit fee in case of type "REFUND". - refund_fee?: Amount; + } - // Hash over the proposal data of the contract that - // is being paid (if type is "DEPOSIT") or refunded (if - // ``type`` is "REFUND"); otherwise absent. - h_contract_terms?: HashCode; + .. ts:def:: CoinRefundTransaction - // Refund transaction ID. Only present if ``type`` is - // "REFUND". - rtransaction_id?: Integer; + interface CoinRefundTransaction { + type: "REFUND"; - // Coin blinding key. Only present if ``type`` is - // "RECOUP" or "RECOUP-REFRESH". - coin_blind?: DenominationBlindingKeyP; + // The total amount of the coin's value restored + // by this transaction. + // The amount given excludes the transaction fee. + // The current coin value can thus be computed by + // adding the amounts to the coin's denomination value. + amount: Amount; - // Reserve receiving the recoup. Only present if ``type`` is - // "RECOUP". - reserve_pub?: EddsaPublicKey; + // Refund fee in case of type "REFUND". + refund_fee: Amount; + + // Hash over the proposal data of the contract that + // is being refunded. + h_contract_terms: HashCode; + + // Refund transaction ID. + rtransaction_id: Integer; // `EdDSA Signature ` authorizing the REFUND. Made with // the `public key of the merchant `. - // Only present if ``type`` is "REFUND". - merchant_sig?: EddsaSignature; + merchant_sig: EddsaSignature; + + } + + .. ts:def:: CoinRecoupTransaction + + interface CoinRecoupTransaction { + type: "RECOUP"; + + // The total amount of the coin's value absorbed + // by this transaction. + // The current coin value can thus be computed by + // subtracting the amount from + // the coin's denomination value. + amount: Amount; + + // Date when the operation was made. + timestamp: Timestamp; + + // Signature by the coin. + coin_sig: EddsaSignature; + + // Hash of the public denomination key used to sign the coin. + // FIXME: why do we care to have this? + h_denom_pub: HashCode; + + // Coin blinding key. + coin_blind: DenominationBlindingKeyP; + + // Reserve receiving the recoup. + reserve_pub: EddsaPublicKey; + + // Signature by the exchange, must be + // of type TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP. + exchange_sig: EddsaSignature; + + // Public key used to sign ``exchange_sig`` + exchange_pub: EddsaPublicKey; + + } + + .. ts:def:: CoinOldCoinRecoupTransaction + + interface CoinOldCoinRecoupTransaction { + type: "OLD-COIN-RECOUP"; + + // The total amount of the coin's value restored + // by this transaction. + // The current coin value can thus be computed by + // adding the amount to the coin's denomination value. + amount: Amount; + + // Date when the operation was made. + timestamp: Timestamp; + + // Signature by the exchange + // of type TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH. + exchange_sig: EddsaSignature; + + // Public key used to sign ``exchange_sig``, + exchange_pub: EddsaPublicKey; + + } + + .. ts:def:: CoinRecoupRefreshTransaction + + interface CoinRecoupRefreshTransaction { + type: "RECOUP-REFRESH"; + + // The total amount of the coin's value absorbed + // by this transaction. + // The current coin value can thus be computed by + // subtracting this amounts from + // the coin's denomination value. + amount: Amount; - // Public key of the reserve that will receive the funds, for "RECOUP" operations. - reserve_pub?: EddsaPublicKey; + // Date when the operation was made. + timestamp: Timestamp; + + // Signature by the coin. + coin_sig: EddsaSignature; - // Signature by the exchange, only present if ``type`` is "RECOUP", - // "OLD-COIN-RECOUP" or "RECOUP-REFRESH". Signature is - // of type TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP for "RECOUP", - // and of type TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH otherwise. - exchange_sig?: EddsaSignature; + // Hash of the public denomination key used to sign the coin. + // FIXME: why do we care to have this? + h_denom_pub: HashCode; + + // Coin blinding key. + coin_blind: DenominationBlindingKeyP; + + // Signature by the exchange + // of type TALER_SIGNATURE_EXCHANGE_CONFIRM_RECOUP_REFRESH. + exchange_sig: EddsaSignature; // Public key used to sign ``exchange_sig``, - // only present if ``exchange_sig`` present. - exchange_pub?: EddsaPublicKey; + exchange_pub: EddsaPublicKey; - // Blinding factor of the revoked new coin, - // only present if ``type`` is "REFRESH_RECOUP". + // Blinding factor of the revoked new coin. new_coin_blinding_secret: RsaBlindingKeySecret; - // Blinded public key of the revoked new coin, - // only present if ``type`` is "REFRESH_RECOUP". + // Blinded public key of the revoked new coin. new_coin_ev: RsaBlindingKeySecret; } + .. ts:def:: CoinPursePaymentTransaction + + interface CoinPursePaymentTransaction { + type: "PURSE_PAYMENT"; + + // The total amount of the coin's value absorbed + // by this transaction. + // Note that this means the amount given includes + // the deposit fee. The current coin value can thus be computed by + // subtracting the amount from + // the coin's denomination value. + amount: Amount; + + // Deposit fee. + deposit_fee: Amount; + + // Public key of the purse. + purse_pub: EddsaPublicKey; + + // Date when the purse was set to expire. + purse_expiration: Timestamp; + + // Signature by the coin. + coin_sig: EddsaSignature; + + // Hash of the public denomination key used to sign the coin. + // FIXME: why do we care to have this? + h_denom_pub: HashCode; + + } + + + ---------- Refreshing ---------- @@ -2185,12 +2432,12 @@ Wallet-to-wallet transfers TODO for the spec: - * Update coin history replies to include purse actions: - - TALER_SIGNATURE_PURSE_PAYMENT! - * Update reserve history replies to include merge & kyc actions: - - TALER_SIGNATURE_ACCOUNT_MERGE - - TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST + * add reserve history requests (with fee!) + to reserve history (changes balance!) * specify new database schema at exchange (add SQL to DD13!) + - something for in-progress kyc vs. completed kyc? + => add kyc_date to reserves? + => or have separate KYC table instead of NULLs in reserves! * update wire transfer API to enable WAD IDs (and while we are at it, should probably also write extended version to allow _merchants_ to query for their inbound transfers, so spec @@ -2544,6 +2791,12 @@ Discussion: // Must be of purpose TALER_SIGNATURE_PURSE_MERGE. purse_sig: EddsaSignature; + // Minimum amount that must be credited to the reserve, that is + // the total value of the purse minus the deposit fees. + // If the deposit fees are lower, the contribution to the + // reserve can be higher! + minimum_amount_contributed: Amount; + // SHA-512 hash of the contact of the purse. h_contract_terms: HashCode; @@ -2677,10 +2930,10 @@ Discussion: // this is a ``Bad Request`` (HTTP status 400). payto_uri: string; - // EdDSA signature of the account affirming the request + // EdDSA signature of the reserve affirming the request // to create the account, must be of purpose // TALER_SIGNATURE_ACCOUNT_SETUP_REQUEST - account_sig: EddsaPublicKey; + reserve_sig: EddsaPublicKey; } @@ -2787,9 +3040,9 @@ wallet-to-wallet payments. Only another exchange should access this endpoint. // Client-side timestamp of when the merge request was made. merge_timestamp: Timestamp; - // Signature created with the account's private key. + // Signature created with the reserve's private key. // Must be of purpose TALER_SIGNATURE_ACCOUNT_MERGE - account_sig: EddsaSignature; + reserve_sig: EddsaSignature; // Signature created with the purse's private key. // Must be of purpose TALER_SIGNATURE_PURSE_MERGE diff --git a/design-documents/013-peer-to-peer-payments.rst b/design-documents/013-peer-to-peer-payments.rst index 0d3a767c..3a998c78 100644 --- a/design-documents/013-peer-to-peer-payments.rst +++ b/design-documents/013-peer-to-peer-payments.rst @@ -19,7 +19,7 @@ This will be used for payments via e-mail and other messaging apps, as well as possibly for transfers via NFC/QR code between mobile phones. Invoice Flow User Experience ----------------------------------- +---------------------------- .. graphviz:: @@ -69,7 +69,7 @@ Invoice Flow User Experience } Donation Flow User Experience -------------------------------------- +----------------------------- .. graphviz:: @@ -618,6 +618,104 @@ Additional considerations Taler's "one-hop withdrawal loohole". +Exchange database schema changes +-------------------------------- + +We need to exchange the existing reserves table to include bits for KYC-needed +and KYC-passed. Also, we need to store the payto://-URI of the bank account. + +Finally, we may need to keep some link to the KYC data, even though the +exchange technically does not need it, but likely there might be regulatory +reasons to have that association for legal inquiries. (However, it would +also be possible to keep that link only in the external KYC service's +database.) + + + +.. sourcecode:: sql + + -- Everything in one big transaction + BEGIN; + -- Check patch versioning is in place. + SELECT _v.register_patch('exchange-TBD', NULL, NULL); + -- + ALTER TABLE reserves + ADD COLUMN kyc_needed BOOLEAN NOT NULL DEFAULT (false) + ADD COLUMN kyc_passed BOOLEAN NOT NULL DEFAULT (false) + ADD COLUMN payto_uri TEXT DEFAULT (NULL) + ADD COLUMN kyc_link TEXT DEFAULT (NULL); + COMMENT ON COLUMN reserves.kyc_needed + IS 'set to true once a reserve was merged with a purse'; + COMMENT ON COLUMN reserves.kyc_passed + IS 'set to true once the user performed the KYC check'; + COMMENT ON COLUMN reserves.payto_uri + IS 'bank account details to use in case reserve is closed'; + COMMENT ON COLUMN reserves.kyc_link + IS 'optional link to KYC data'; + -- + CREATE TABLE IF NOT EXISTS kyc_requests + (kyc_request_serial_id BIGSERIAL UNIQUE + ,reserve_pub BYTEA NOT NULL REFERENCES reserves (reserve_pub) ON DELETE CASCADE + ,kyc_date INT8 NOT NULL + ,kyc_fee_val INT8 NOT NULL + ,kyc_fee_frac INT4 NOT NULL + ,payto_uri TEXT NOT NULL + ,reserve_sig BYTEA NOT NULL CHECK (LENGTH(reserve_sig)=64)) + ,PRIMARY KEY (reserve_pub, kyc_date) + ); + CREATE TABLE IF NOT EXISTS mergers + (merge_request_serial_id BIGSERIAL UNIQUE + ,reserve_pub BYTEA NOT NULL REFERENCES reserves (reserve_pub) ON DELETE CASCADE + ,purse_url TEXT NOT NULL, + ,purse_pub BYTEA NOT NULL CHECK (LENGTH(purse_pub)=32), + ,reserve_sig BYTEA NOT NULL CHECK (LENGTH(reserve_sig)=64)) + ,purse_sig BYTEA NOT NULL CHECK (LENGTH(reserve_sig)=64)) + ,merge_timestamp INT8 NOT NULL + ,purse_expiration INT8 NOT NULL + ,h_contract_terms BYTEA NOT NULL CHECK (LENGTH(h_contract_terms)=64)) + ,h_wire BYTEA NOT NULL CHECK (LENGTH(h_contract_terms)=64)) + ,purse_val INT8 NOT NULL + ,purse_frac INT4 NOT NULL + ,PRIMARY KEY (purse_pub) + ); + CREATE TABLE IF NOT EXISTS contracts + (contract_serial_id BIGSERIAL UNIQUE + ,purse_pub BYTEA NOT NULL CHECK (LENGTH(purse_pub)=32), + ,pub_ckey BYTEA NOT NULL CHECK (LENGTH(pub_ckey)=32)), + ,e_contract BYTEA NOT NULL, + ,PRIMARY KEY (purse_pub) + ); + CREATE TABLE IF NOT EXISTS history_requests + (reserve_pub BYTEA NOT NULL CHECK (LENGTH(purse_pub)=32), + ,request_timestamp INT8 NOT NULL + ,reserve_sig BYTEA NOT NULL CHECK (LENGTH(reserve_sig)=64)) + ,PRIMARY KEY (reserve_pub,request_timestamp) + ); + CREATE TABLE IF NOT EXISTS purse_deposits + (purse_deposit_serial_id BIGSERIAL UNIQUE + ,purse_pub BYTEA NOT NULL CHECK (LENGTH(purse_pub)=32), + ,purse_expiration INT8 NOT NULL + ,coin_pub BYTEA NOT NULL REFERENCES known_coins (coin_pub) ON DELETE CASCADE + ,amount_with_fee_val INT8 NOT NULL + ,amount_with_fee_frac INT4 NOT NULL + ,coin_sig BYTEA NOT NULL CHECK(LENGTH(coin_sig)=64) + ,PRIMARY KEY (purse_pub,coin_pub) + ); + CREATE TABLE IF NOT EXISTS wads + (wad_serial_id BIGSERIAL UNIQUE + ,reserve_pub BYTEA NOT NULL REFERENCES reserves (reserve_pub) ON DELETE CASCADE + ,kyc_date INT8 NOT NULL + ,kyc_fee_val INT8 NOT NULL + ,kyc_fee_frac INT4 NOT NULL + ,payto_uri TEXT NOT NULL + ,reserve_sig BYTEA NOT NULL CHECK (LENGTH(reserve_sig)=64)) + ,PRIMARY KEY (reserve_pub, kyc_date) + ); + -- Complete transaction + COMMIT; + + + Alternatives ============ -- cgit v1.2.3