Minka Ledger Docs
Explanations

About Intents

DateResponsibleChanges
December 20, 2022@Tomislav HermanInitial version
January 19, 2023@Branko Durdevic @Tomislav HermanUpdated intent states, event names, and updated json payloads of events and signatures.
January 26, 2023@Tomislav HermanUpdated JSON examples with new signature schema. Added more detailed explanations and examples for messages exchanged in 2PC. Collapsed code examples into toggle lists.
January 27, 2023@Tomislav HermanAdded action property to event data which needs to be matched by the related signature added by the bridge. Updated JSON examples with new action property.
February 15, 2023@Tomislav HermanUpdated naming: introduced operation an entry. Updated payload of prepare operation: removed ‘action’ and added ‘schema’, removed ‘select’. Updated payloads of commit/abort operations: not they contain just ‘handle’ and ‘action’. Removed ‘action’ from bridge signatures.
February 17, 2023@Tomislav HermanUpdated naming: changed ‘operation’ into `request'. Updated rest interface examples with '/v2' prefix to follow bridge api versioning.
March 7, 2023@Filip Herceg• Refactored signature.schemasignature.method, meta.signaturesmeta.proofs
• Refactored signer.schemasigner.format
• Added custom.moment to examples
March 7, 2023@Omar MonterreyUpdated schema of bridge payloads (target, source and symbols are now objects)
March 9, 2023@Tomislav HermanAdded intent to payload of commit/abort commands. Updated all API calls with /v2 prefix.
April 24, 2023@Tomislav HermanRemoved signer from bridge.config example and updated descriptions to not mention this property. Added sections which explain the access requirements for adding the 2PC related signature to intents (sign-intent action).
August 22, 2023@Omar MonterreyReplaced references to action sign-intent to sign on record intent
September 19, 2023@Luis FidelisReplaced /v2/intents/:handle/sign endpoint to /v2/intents/:handle/proofs
December 13, 2023@Luis Fidelis• Added description of intent status prepared
• Added description of intent config object
• Added details about intent action proofs to commit and abort with examples

What are intents

Intents represent a way to execute a change in ledger. Most common changes involving intent is the movement of balances between wallets.

Intent author constructs and signs the intent with his private key that way ensuring integrity and authenticity of his intention.

Intents guarantee atomicity of changes in ledger, meaning that if there are multiple changes they will either be all persisted completely or no change will be persisted at all.

There are 6 intent statuses related to atomicity:

StatusDescription
PendingIntent is submitted but still being processed. Ledger data changes are not yet persisted.
PreparedIntent is prepared by all participants and can be processed. Ifconfig.commitisautoor not defined, it will be committed. Ifmanual, ledger awaits request proof to abort or commit. See intent config for details.
CommittedIntent is processed successfully. Ledger data changes are persisted and irreversible. Will eventually be completed.
CompletedIntent was successfully processed.
AbortedIntent was aborted while in pending status. Will eventually be rejected.
RejectedIntent processing failed. Ledger data changes are rejected and cannot be persisted.

completed and rejected are so called final statuses and no changes to an intent are possible after an intent ends up in one of those.

Intent claims

Claim is the smallest unit of change stated in intent. Intent can have one or multiple claims which can be used to represent anything from simple to complex data changes in ledger. Atomicity property mentioned above ensures that either all claims will be executed or none.

The simplest example of an intent with 1 claim is a transfer of balance between two wallets.

Example of an intent with 2 claims can be an exchange transaction where 2 wallets exchange one currency for another at an agreed rate where the atomicity property plays an important role of ensuring that one party in the transaction can’t be tricked by another.

An example with 3 claims can be similar to the exchange with the addition that the provider of exchange can take a fee for the exchange.

Intent config

Intent supports a configuration object config designed to define specific characteristics and behaviors. This property is optional and has the following properties

  • commit - defines whether the intent should be processed automatically or if ledger should await for a manual action to commit or abort. It is checked after receiving all the required signatures to prepare the intent and its in prepared status.
    • auto (default) - intent will be automatically committed after preparation.
    • manual - ledger should await for a request proof to commit or abort and cannot be processed automatically.

Intent signatures

Intent signature is a proof which guarantees authenticity and integrity of the intent. In other words it guarantees:

  • that the signer is really the author or witness of the intent and that he agreed with it
  • that the intent is not tampered with in transport or at rest

Intent signatures are also the mechanism to achieve the agreement between multiple parties involved in the intent without the need to trust to third party mediator. In our example of exchange intent where 2 wallets are involved, signatures from owners of both wallets will be required in order to clear the intent.

Both parties know that the intent needs 2 signatures in order to be cleared and each will independently decide whether the terms of the exchange are acceptable and confirm that with their signature.

Required signatures for intent are determined by the content of the intent claims. For example, a transfer claim which transfers balance from source to target requires a signature which has permission to perform spend action on the source wallet. Security rules which define which signers have the permission to spend a wallet is configured through authorization rules.

Intent processing phases

Intent is processed in 3 sequential steps:

  1. Aspects
  2. Two-phase commit (2PC)
  3. Effects

Aspects for the intent are executed first as a precondition for accepting the intent. They should check preconditions for an intent such as schema validity, business rules and limits. Aspects are executed synchronously and ledger waits for results of aspects. Error returned from aspect will abort the intent processing and put it into aborted status.

Two-phase commit step is executed after all aspects are successful. It is used to coordinate distributed commit of the intent across multiple ledgers or systems. In case of new intents added during this step, for each new intent the aspects for this intent will be executed first and after that 2PC will be continued to cover these new intents also. If any participant reports a failure during that step, intent will be aborted and all participants will get abort request.

Effects for the intent will be executed after intent is successfully committed and changed to committed/completed status. Effects are used just to notify listeners about ledger change and in this step they can’t affect the result of ledger operations. If intent fails during first 2 steps and becomes aborted/rejected, effects can also be executed to notify listeners about that.

Ledger intent processing -Processing (2).png

prepare entries are sent first to all the debit sources and then to the credit targets.On the other hand, abort entries are sent in reverse order. First to credit targets and then to debit sources. commit calls are sent in parallel to all the participants.

Cross-ledger intent commits

Often, changes in one ledger are accompanied or backed by changes in another, external ledger. For example, let’s imagine a clearing system for interbank transfers implemented on ledger (RTP - Real Time Payment) where banks A and B are participating. This ledger will keep the balances for both banks and update them when a transfer occurs between them. If a user from bank A wants to send money to user in bank B, balance of bank A will decrease while balance of bank B will increase by the same amount in the RTP ledger. At the same time, bank A will debit the user account in their own ledger to cover the RTP balance decrease while the bank B will credit their user account in their own ledger as a response to RTP balance increase. Similar process is also performed in double-entry bookkeeping system. We want atomicity of this distributed transaction so that either all 3 participants will update their ledger records or none of them will.

Ledger has the ability to coordinate such distributed commits by employing 2PC (2-phase commit protocol) as a precondition before clearing an intent. 2PC process has 2 phases: prepare + commit.

Prepare phase is used to notify participants about the intent and let them prepare and agree with it. commit phase is used to notify participants that intent should be committed on their side. If a participant replied positively during the prepare phase it is assumed that he can’t fail after that, meaning that he has to commit the transaction on his side after receiving the commit request.

From ledger perspective, this is performed by the following steps:

  1. After intent is submitted and successfully verified, ledger will identify all participants involved in distributed commit of the intent and construct debit and credit entries for participants
  2. Ledger sends prepare request to all participants. Participants which is referenced as a source of balance movement will get prepare request for debit entry, and participants which are referenced as target will get prepare request for credit entry.
  3. Participant decides whether it will process the entry, makes the preparations on their side (reservation of funds etc.) and replies prepared
  4. After ledger receives prepared from all participants, ledger checks intent config flag commit
    1. auto or not defined - the ledger clears and commits the intent and sends commit request to all participants so they can commit the prepared entry
    2. manual - the ledger awaits for a request proof to commit or abort
  5. Every participant commits the entry on their side and replies with committed

Intent can also fail if one of participants responds with failed after they receive prepare or if timeout happens while waiting for confirmations from participants. In that case step 4 and next steps are different:

  1. Ledger discards all pending changes for intent, aborts it and sends abort request to all participants
  2. Every participant discards prepared changes, aborts the entry on their side and replies with aborted

Ledger intent processing -2PC (2).png

Requests and confirmations in detail:

  • prepare - Ledger uses one of the transport mechanisms which are also used for effects. In case of REST, the POST /v2/debits or POST /v2/credits endpoints are called depending on whether the participant represents source or target of balance movement and the entry is sent in the payload. Debit entry contains the source address and the credit entry contains target address so the participant knows which account to debit or credit in their system. After that ledger awaits for prepared confirmation from participant. Entry has a handle which is unique identifier of the entry and is used for idempotency.
  • prepared - Participant signals that it validated and prepared the entry (made reservations, etc…) by adding a signature to the intent referenced in the entry. Signature has custom.status = 'prepared' and other fields where they reference the received entry. It also adds a reference to the operation in their internal system related to this preparation by setting custom.coreId in the signature.
  • failed - Participant rejects processing of the entry after it receives prepare by adding a signature with custom.status = 'failed' to the intent with additional rejection reason in custom.reason and custom.detail, and their internal error code in custom.failId if applicable.
  • commit - Ledger notifies the participant that entry should be committed. In case of REST, the POST /v2/debits/:handle/commit or POST /v2/credits/:handle/commit endpoints are called. Payload contains the handle and the action which has the value 'commit'.
  • abort - Ledger notifies the participant that the entry should be aborted. In case of REST, the POST /v2/debits/:handle/abort or POST /v2/credits/:handle/abort endpoints are called. Payload also contains the handle and the action which has the value 'abort'.
  • committed - Participant replies to commit by adding a signature to an intent with custom.status = 'committed'. It’s expected from a participant to always respond to commit with committed as a proof of successful commit on their side. It can’t do anything to change the outcome of intent at this moment. Ledger will retry commit if participant doesn’t respond with committed. Participant can add additional field custom.coreId to signature in order to reference operation in their internal system related to this commit.
  • aborted - Participant replies to abort by adding a signature to an intent with custom.status = 'aborted'. It’s expected from a participant to always respond to abort with aborted as a proof of successful abort on their side. It can’t do anything to change the outcome of intent at this moment. Ledger will retry abort if participant doesn’t respond with aborted. Participant can also add additional field custom.coreId to signature in order to reference operation in their internal system related to this abort.

Payload sent in prepare requests

Although different transport mechanism can be used to deliver this request to participants, the payload of the request always has the same JSON structure representing the entry record signed by the ledger. The data of the entry record contains the following fields:

  • handle - unique handle of the entry, used for idempotency, prepare and corresponding commit request will contain the same handle
  • schema - identifier of the entry schema, represents the standard entry types in double-entry bookkeeping system, it can be: debit or credit
  • source - source of balance movement in ledger, used only in debit entries
  • target - target of balance movement in ledger, used only in credit entries
  • symbol - symbol, unit of balance
  • amount - amount of balance, it is a sum of amount from all intent claims which has the same source in case of debit entry, or the same target in case of credit entry
  • intent - complete intent record which is the cause of balance movement

Examples of payloads for different prepare requests are given below:

  • POST /v2/debits

    {
        "hash": "...",	 
        "data": {
    				"handle": "deb_baC0LUTVW9lzYA284",
    				"schema": "debit",
    				"source": {
    					"handle": "account:1234@bank1.com"
    				},
    		    "symbol": {
    					"handle": "usd"
    				},
    				"amount": 100,
    				"intent": {
    						"hash": " ... ",
    						"data": { ... },
    						"meta": {
    								"status": "pending",
    				       "thread": "YJxAMz52rMPiNYOZV",
    								"proofs": [ ... ]
    						}
    				}
        },
        "meta": { ... }
    }
  • POST /v2/credits

    {
        "hash": "...",	 
        "data": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"schema": "credit",
    				"target": {
    					 "handle": "account:1234@bank1.com"
    				},
    		    "symbol": {
    					 "handle": "usd"
    				},
    				"amount": 100,
    				"intent": {
    						"hash": " ... ",
    						"data": { ... },
    						"meta": {
    								"status": "pending",
    				       "thread": "YJxAMz52rMPiNYOZV",
    								"proofs": [ ... ]
    						}
    				}
        },
        "meta": { ... }
    }

Payload sent in commit and abort requests

These requests contain the command related to previous entry. The payload also has standard record structure which is signed by the ledger. The data of the command record contains the following fields:

  • handle - unique handle of the entry previously sent in prepare request
  • action - action which needs to be performed with the entry, it can be: commit or abort
  • intent - complete intent record which is the cause of balance movement with current status and signatures

Examples of payloads for different commit and abort requests are given below:

  • POST /v2/debits/:handle/commit

    {
        "hash": "...",	 
        "data": {
    				"handle": "deb_baC0LUTVW9lzYA284",
    				"action": "commit",
    				"intent": {
    						"hash": " ... ",
    						"data": { ... },
    						"meta": {
    								"status": "committed",
    				       "thread": "YJxAMz52rMPiNYOZV",
    								"proofs": [ ... ]
    						}
    				}
        },
        "meta": { ... },
    }
  • POST /v2/credits/:handle/commit

    {
        "hash": "...",	 
        "data": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"action": "commit",
    				"intent": {
    						"hash": " ... ",
    						"data": { ... },
    						"meta": {
    								"status": "committed",
    				       "thread": "YJxAMz52rMPiNYOZV",
    								"proofs": [ ... ]
    						}
    				}
        },
        "meta": { ... },
    }
  • POST /v2/debits/:handle/abort

    {
        "hash": "...",	 
        "data": {
    				"handle": "deb_baC0LUTVW9lzYA284",
    				"action": "abort",
    				"intent": {
    						"hash": " ... ",
    						"data": { ... },
    						"meta": {
    								"status": "aborted",
    				       "thread": "YJxAMz52rMPiNYOZV",
    								"proofs": [ ... ]
    						}
    				}
        },
        "meta": { ... },
    }
  • POST /v2/credits/:handle/abort

    {
        "hash": "...",	 
        "data": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"action": "abort",
    				"intent": {
    						"hash": " ... ",
    						"data": { ... },
    						"meta": {
    								"status": "aborted",
    				       "thread": "YJxAMz52rMPiNYOZV",
    								"proofs": [ ... ]
    						}
    				}
        },
        "meta": { ... }
    }

Signature used for prepared, failed, committed, aborted responses

Request confirmation response from 2PC participant is implemented by adding a signature to the intent proofs by the participant. 2PC coordinator keeps track of participant confirmations by verifying that they contain the correct handle related to the entries sent to participant.

Signature is added to intent proofs by calling ledger endpoint:

POST /v2/intents/:handle/proofs

Signature has a standard structure with addition of custom fields related to 2PC confirmation:

  • method - reference to cryptographic algorithm
  • public - public key of the signer
  • digest - digest of the signature, created from intent.data and signature.custom
  • result - signature value
  • custom.handle - handle of the corresponding entry received from ledger
  • custom.status - status reported to ledger as a response to a request, can be: prepared, failed, committed or aborted
  • custom.coreId - reference in the participant system related to received request, can be internal core transaction ID or similar, used for referencing
  • custom.reason - error reason code reported back to ledger, sent together with failed status, can be one of predefined ledger error reasons with prefix bridge., see Errors
  • custom.detail - error detail message which describes the error
  • custom.failId - reference to error in the participant system, can be internal error code, used for referencing
  • custom.moment - date-time representing the moment when the signature is calculated and added to a record. It has a value in ISO datetime format expressed as UTC with millisecond precision, for example: 2023-02-20T21:42:10.279Z

For each intent signature with custom.handle the signer who adds a signature must have granted action sign for record intent on the bridge record which is related to the entry. Otherwise the request to add signature will be rejected. See About Authorization for more details about access rules.

Examples of signatures for different types of confirmations are given below:

  • debit prepared

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "deb_baC0LUTVW9lzYA284",
           "status": "prepared",				
    				"coreId": "83147512640",
    				"moment": "2023-02-20T21:42:10.279Z"
        }
    }
  • credit prepared

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"status": "prepared",				
    				"coreId": "83147512640",
    				"moment": "2023-02-20T21:42:10.279Z"
    		}
    }
  • debit committed

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "deb_baC0LUTVW9lzYA284",
    				"status": "committed",				
    				"coreId": "83147512640",
    				"moment": "2023-02-20T21:42:10.279Z"
    		}
    }
  • credit committed

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"status": "committed",				
    				"coreId": "83147512640",
    				"moment": "2023-02-20T21:42:10.279Z"
    		}
    }
  • debit failed

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "deb_baC0LUTVW9lzYA284",
    				"status": "failed",
    				"coreId": "83147512640",
    				"reason": "bridge.account-insufficient-balance",
    				"detail": "Insufficient balance on the source account",
    				"failId": "6702",
    				"moment": "2023-02-20T21:42:10.279Z"
        }
    }
  • credit failed

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"status": "failed",
    				"coreId": "83147512640",
    				"reason": "bridge.account-inactive",
    				"detail": "Target account is inactive",
    				"failId": "4011",
    				"moment": "2023-02-20T21:42:10.279Z"
        }
    }
  • debit aborted

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "deb_baC0LUTVW9lzYA284",
    				"status": "aborted",
    				"coreId": "83147512640",
    				"moment": "2023-02-20T21:42:10.279Z"
    		}
    }
  • credit aborted

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"handle": "cre_baC0LUTVW9lzYA284",
    				"status": "aborted",				
    				"coreId": "83147512640",
    				"moment": "2023-02-20T21:42:10.279Z"
        }
    }

Signature used for requesting a commit or abort

Request an action on an intent is implemented by adding a signature to the intent proofs by the signer who has access granted to commit or abort. These actions are checked by the ledger when intent config commit flag is manual and can be sent either when the intent is pending or prepared

Signature is added to intent proofs by calling ledger endpoint:

POST /v2/intents/:handle/proofs

Signature has a standard structure with addition of custom fields related to the target action

  • method - reference to cryptographic algorithm
  • public - public key of the signer
  • digest - digest of the signature, created from intent.data and signature.custom
  • result - signature value
  • custom.status - status of signature should be requested
  • custom.action - target action. It can be commit or abort
  • custom.coreId - reference in the participant system related to received request, can be internal core transaction ID or similar, used for referencing
  • custom.reason - error reason code reported back to ledger, sent together with abort action, can be one of predefined ledger error reasons with prefix bridge., see Errors
  • custom.detail - error detail message which describes the error
  • custom.failId - reference to error in the participant system, can be internal error code, used for referencing
  • custom.moment - date-time representing the moment when the signature is calculated and added to a record. It has a value in ISO datetime format expressed as UTC with millisecond precision, for example: 2023-02-20T21:42:10.279Z

Examples of signatures for the two types of action requests

  • commit request

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"status": "requested",
           "action": "commit",
    				"moment": "2023-02-20T21:42:10.279Z"
        }
    }
  • abort request

    {
    		"method": "ed25519-v2",
    		"public": "...",
    		"digest": "...",
    		"result": "...",
    		"custom": {
    				"status": "requested",
           "action": "abort",
    				"moment": "2023-02-20T21:42:10.279Z"
        }
    }

Identifying participants in 2PC

2PC will be performed for intent with balance moving claims where source or target wallet has a bridge reference. Bridge is an entity registered in ledger which represents an external system or ledger which needs to execute entries as a response to balance movements in ledger. It is defined by config.server: the base url where bridge listens for ledger requests.

  • Example bridge record

    // BRIDGE
    {
        "hash": "...",   
        "data": {
            "handle": "hpb",
            "schema": "rest",
            "config": {
                "server": "https://example.com/v2"
            },
            "secure": [ ... ],
            "access": [{
                "action": "sign-intent",
                "signer": {
                    "public": "<bridge-public>"
                }
            }]
        },
        "meta": { ... }
    }

There is also access rule which grants the bridge public key to add confirmation signatures to an intent in the name of the bridge { "action": "sign", "record": "intent" }

  • Example wallet with bridge reference

    {
    		"hash": "...",
    		"data": {
    				"handle": "hpb",
    				"bridge": "hpb",
    				"access": [ ... ]				
        },
    		"meta": { ... }
    }

Each bridge will be included in 2PC once per every occurrence either as the source or the target and also once per each different address it is associated with. For example:

  • If same bridge is associated with both source and target of the intent claim, them it will be included in 2PC twice, once for debit on source and once for credit on target.
  • If same bridge is associated with sources of two intent claims but the source addresses are different, then it will be also included twice in 2PC, once for debit on source address of the first claim and once for debit on source address of the second claim. (same pattern for credit)
  • If same bridge is associated with sources of two intent claims but the source addresses are the same, then it will be included only once for debit on this source address in 2PC. Received entry in prepare request will then contain aggregated amount from both claims. (same pattern for credit)
  • Any combination of 3 cases above is possible also with many claims and bridges.

Collecting confirmations from participants

Transaction coordinator will expect prepared response from every participant to which it sent prepare request. After all participants respond with prepared it will go to next phase where it will try to determine whether all required signatures are collected in order to perform spend on all involved source wallets. Signature requirements for spending from the wallet are configured through authorization rules described here: About Authorization.

If some signatures are still missing after all participants responded, the intent will still be pending and commit phase will be delayed until all required signatures are collected, or timeout happens in the meantime. When finally all signatures are collected, commit phase will start and all participants will get commit request in order to complete the entry processing on their side.

Ledger 2PC roles

Ledger 2PC Coordinator

Ledger has a role of 2PC transaction coordinator which communicates with participants and manages the prepare and commit phase.

Ledger TXN Core - participant

Ledger also has a participant role in 2PC in which it behaves as any external ledger. When it receives prepare request, it will validate that there are enough balances in the wallets in order to clear all claims in an intent, and then make a reservation of these balances so that these balances can’t be spent by other intents. After that it will respond with prepared, by adding a signature to the intent confirming that all balance movements stated in claims are ready to be committed. After that, when it receives commit from coordinator, it will persist changes of balances between the wallets.

Ledger TXN core is default participant in 2PC for every intent so it will automatically be notified with prepare request when 2PC starts.

Intent threads

Used to span 2PC protocol across multiple intents and that way guaranteeing atomicity over the set of intents. Sometimes a participant, as a precondition to accepting the distributed commit of intent in the prepare phase could add additional intent to be executed as the part of the same commit. For example, we use this mechanism in ledger routes where a new intent is added in case an outgoing route is defined on the target.

Every new intent is added to a thread by referencing original intent through origin property of the intent with the value of the previous intent handle. Original intent will not have this property.

Every new intent added to thread can then require additional participants which need to be prepared for a distributed commit. This process can repeat until all participants, including additional, become prepared and no new intents/participants are added. The set of the intents built that way is called INTENT THREAD and it’s committed atomically.

prepare requests are sent for each new intent in the thread, and each intent is prepared separately by the participants. So, it’s possible that one participant gets multiple prepare operations.