``` PIP: XX Title: PascalCoin Multi-Approval Escrow Type: Protocol Impact: Hard-Fork Author: Benjamin Ansbach Status: Proposal Created: 2019-11-21 ``` ## Abstract This PIP proposes a simple, less-intrusive (development wise), multi-approval escrow sub-protocol for up to 6 participants. It will use the existing safebox capabilities and can be used with all available key types. ## Copyright WTFPL ## Presumptions - The safebox information is always present on a node. - We cannot use the account name to record data as it has to be globally unique. Therefore 32 bytes `SafeBox.Account.data` field will be used. - A reservation for an `SafeBox.Account.type` value is made (500). - Reservations for `BlockChain.Operation[DATA].data_type` is made (500, 501 and 502). ## Short description By saving an escrow definition in the `SafeBox.Account.data` field and setting the `SafeBox.Account.type` to `500` the account will be in a new state called `escrow`. An Account in this state will have limited capabilities which are enforced by the node consensus implementation for this type of account. It is less-intrusive because it's implementation is much simpler than adding MultiSig to the core and it works with any already existing Pascal key type. There are more solutions to this problem, especially by extending the SafeBox with more fields, but the author did not go that way on purpose. The proposed solution is also completely trust-less, the third party can only act on behalf of the participants decisions: This is either.. - ..to send the escrow-locked amount to the beneficiary if enough participants agree or - ..to send the escrow-locked amount back to it's original account. ## Setting up a multi-approval escrow PASA The following will describe the workflow to set up an escrow account. 1. **Obtain an account** Obtain an account and name it as you wish, e.g. `ESCROW-ORDER-123` and make sure it's empty: 1. `SafeBox.account.data = ''` 2. `SafeBox.account.balance = 0` 3. `SafeBox.account.type = 0` After the escrow-process is finished, the same account can be used for another escrow-process. 2. **Initiate a multi-approval escrow** Set the escrow definition and save it into `SafeBox.account.data` of the account: 1. `min_approvals` The minimum number of approvals required for the account owner to dissolve the escrow. 2. `origin` The account number where the balance originally came from and the amount can be send back to. 3. `beneficiary` The account number of the potential beneficiary where the balance will be send to when there are enough approvals available. 4. `participants` A list of maximum 5 account numbers that can approve or disapprove the payment. 5. `flags` A list of 8 possible flags 1. `owner_can_approve` A flag indicating whether the owner of the escrow is able to take part in the approval process, increasing the number of possible participants to 6. 2. .. TBD 6. `count_approvals (PROP2 ONLY)` The number of recorded approvals. On setup, this value must be set to zero. 3. **Change account type to 500** Set the `SafeBox.Account.type` value to 500 (escrow). 4. **Send the escrow amount** Send the amount to be locked to the escrow account from the `SafeBox.Account.data.origin` account. ### Escrow state As soon as the last step of the workflow above is completed, it will be considered a locked escrow account. An active escrow account is defined with the following conditions: - `SafeBox.Account.type = 500` - a valid `SafeBox.Account.data` entry according to the escrow protocol specification. - `SafeBox.Account.balance > 0` Locked means, the account owner agreed to the protocol definition and also agreed to restrict the account to a limited set of operations: - A Transaction of `SafeBox.Account.balance` back to `SafeBox.Account.data.origin` - A Transaction of `SafeBox.Account.balance` to `SafeBox.Account.data.beneficiary` (in case there are enough approvals, explained below) - A DATA operation to approve or disapprove a payment when the account is part of the participants (`SafeBox.Account.data.owner_can_auth`) ## Approve/Disapprove a payment Any of the 5 mentioned accounts in `SafeBox.Account.data.participants` (plus the owner of the escrow-account if `SafeBox.Account.data.flags.owner_can_auth` is set) can issue an approval and initiate the dissolvement of the escrow by sending a DATA operation to the escrow account. The DATA operation to approve an escrow will have the `data_type` set to `500`. The payload contains the following values: - `approval` A bool value indicating whether the account gives the approval or not. - `subject` Additional arbitrary information (e.g. reason for rejection, reason for approval). When enough accounts send an approval operation (`SafeBox.Account.data.min_approvals`), the owner of the escrow account can issue a transaction of the full amount to `SafeBox.account.data.beneficiary`. The owner will **not** be able to send the amount back to the `SafeBox.account.data.origin` once there are enough approvals. A technical definition of this payload "protocol" can be found at the end of the document. ## Finish an escrow This approach reminds one of a smart contract functionality. The idea is that whenever an approval is sent to the escrow account, the value of `SafeBox.Account.data.count_approvals` is incremented by one and the sender is removed from `SafeBox.Account.data.participants`. Therefore that the node will mutate the state of an account with each incoming approval operation. When the escrow owner sees that there are enough approvals available (via `SafeBox.Account.data.count_approvals`), he can issue a full payment to the beneficiary by sending a DATA operation with `data_type` = 502 with a payload of his choice. The node will check if - `BlockChain.Operation.optype = 501` - `BlockChain.Operation.amount + BlockChain.Operation.fee = SafeBox.Account.balance` - the sender accounts `SafeBox.Account.data.count_approvals` is greater or equal the minimum required approvals. When there are enough valid approvals per escrow definition and the operation is valid, the node will include the operation. If not it will be rejected. One could also think about automatically releasing the balance to the beneficiary, but this can be up to discussion. A more technical example implementation is available at the end of the document. ## Cancel an escrow An escrow can be canceled as long as there are not enough approvals available. It is canceled by sending the amount back to `SafeBox.Account.data.count_approvals` ## Protocol specification ### Escrow To setup an escrow, the `SafeBox.Account.data` field will be initiated with the following data which ends up with 32 Bytes at most, which is the size of the field. | Field | Type | Size | | ------------------------------------------------------------ | --------------------------------- | ---------- | | `min_approvals`<br />The minimum number of approvals required for the owner to send the balance to the beneficiary. | `uint8` | 1 byte | | `origin`<br />The account that originally sent the amount. | `uint32 LE` | 4 bytes | | `beneficiary`<br />The beneficiary account that will get the balance once there are enough approvals. | `uint32 LE` | 4 bytes | | `participant_count`<br />The number of participant accounts. | `uint8` | 1 byte | | `participants`<br />The list of participant account numbers. | `Array<participant_count>` | 0-20 bytes | | `participants[0..participant_count-1].account`<br />A single participant. | `uint32 LE` | -> 4 bytes | | `flags`<br />A list of (max 8) flags. | `uint8` | 1 byte | | `flags.owner_can_approve`<br />Allow the owner of the escrow pasa to approve. | `flag 1` | 1 bit | | `flags.x`<br />Not defined yet. | `AnyOf<2, 4, 8, 16, 32, 64, 128>` | | | `count_approvals` <br />Initially always 0. | `int8` | 1 byte | ### Approve To approve an escrow the following payload together with a DATA operation with `data_type = 500` should be sent to the escrow account. | Field | | | | ------------------------------------ | -------- | -------------- | | `approve`<br />0 = false, > 0 true | `uint8` | 1 Byte | | `subject`<br />Arbitrary information | `string` | [0..254] bytes | ### Finish To successfully finish an escrow, the PASA owner can send the complete balance to the destination account using a DATA operation with the `data_type = 501`. The payload is irrelevant and can contain any arbitrary data. ### Cancel To cancel an escrow, the PASA owner can send the complete balance to the original account using a DATA operation with the `data_type = 502`. The payload is irrelevant and can contain any arbitrary data. ## Consensus Rules to finish an operation ```bash # .. Other standard rules apply # .. # Further rules to be discussed and validated. # if the account is an escrow account IF operation.optype = 10 AND # DATA operation.data_type IN [501, 502] operation.sender.account.type = 500 # escrow THEN LET escrowAccount = operation.sender # if there is no balance in the account allow everything IF escrowAccount.balance = 0 THEN EXECUTE(operation) ENDIF # parse account data LET escrowData = PARSE_ESCROW(escrowAccount.data) # if the data is invalid it's not a valid escrow IF INVALID(escrowData) THEN EXECUTE(operation) ENDIF # ------------------------------ # we have a valid escrow account # ------------------------------ # allow sending full amount back to origin IF operation.opType = 10 AND operation.dataType = 502 AND operation.target = escrowData.origin AND escrowAccount.balance = operation.amount + operation.fee AND escrowAccount.count_approvals < escrowData.min_approvals THEN EXECUTE(operation) ENDIF # validate sending the balance to the beneficiary IF operation.opType = 10 AND operation.dataType = 501 AND operation.target = escrowData.beneficiary AND operation.sender.balance = operation.amount + operation.fee AND escrowAccount.count_approvals >= escrowData.min_approvals THEN EXECUTE(operation); ENDIF ENDIF ``` ## Example ### Escrow data | Field | Value | HexaString | | ------------------ | ------ | ---------- | | min_approvals | 3 | `03` | | origin | 1000 | `E8030000` | | beneficiary | 666 | `9A020000` | | count_participants | 5 | `05` | | -> participant 1 | 1458 | `B2050000` | | -> participant 2 | 17868 | `CC450000` | | -> participant 3 | 145999 | `4F3A0200` | | -> participant 4 | 254789 | `45E30300` | | -> participant 5 | 98325 | `15800100` | | flags | 0 | `00` | | count_approvals | 0 | `00` | ``` 03E80300009A02000005B2050000CC4500004F3A020045E30300158001000000 ``` **SBX Code:** ```javascript const Coding = require('@pascalcoin-sbx/common').Coding; const Endian = require('@pascalcoin-sbx/common').Endian; const AccountNumber = require('@pascalcoin-sbx/common').Types.AccountNumber; const CompositeType = Coding.CompositeType; class EscrowData extends CompositeType { constructor() { super('escrow'); this.addSubType(new Coding.Core.Int8('minApprovals', true)); this.addSubType(new Coding.Pascal.AccountNumber('origin')); this.addSubType(new Coding.Pascal.AccountNumber('beneficiary')); this.addSubType(new Coding.Core.Int8('participantCount', true)); this.addSubType( new Coding.Repeating('participants', new Coding.Pascal.AccountNumber('participant'), 5, 'participantCount' ) ); this.addSubType(new Coding.Core.Int8('flags', true)); this.addSubType(new Coding.Core.Int8('count_proposals', false) .withFixedValue(0)); } } let escrow = { minApprovals: 3, origin: new AccountNumber(1000), beneficiary: new AccountNumber(666), participantCount: 5, participants: [ new AccountNumber(1458), new AccountNumber(17868), new AccountNumber(145999), new AccountNumber(254789), new AccountNumber(98325) ], flags: 0 }; console.log(new EscrowData().encodeToBytes(escrow).toHex()); // 03E80300009A02000005B2050000CC4500004F3A020045E30300158001000000 ``` ### Approval payload | Field | Value | HexaString | | ------- | -------------------------------------- | ------------------------------------------------------------ | | approve | 1 | 01 | | subject | I hereby approve paying out the escrow | `49206865726562792061`<br />`7070726F766520706179`<br />`696E67206F7574207468`<br />`6520657363726F77` | ``` 01492068657265627920617070726F766520706179696E67206F75742074686520657363726F77 ``` **SBX-Code** ```javascript class ApprovalPayload extends CompositeType { constructor() { super('escrow_approve'); this.addSubType(new Coding.Core.Int8('approve', true)); this.addSubType(new Coding.Core.StringWithoutLength('subject')); } } let approval = { approve: 1, subject: "I hereby approve paying out the escrow" }; console.log(new ApprovalPayload().encodeToBytes(approval).toHex()); // 01492068657265627920617070726F766520706179696E67206F75742074686520657363726F77 ``` ### Finish Payload `PROP1` | Field | Value | HexaString | | ------------------ | ------------- | ---------------------------- | | approval_count | 3 | `03` | | Approval 1 Block | 150001 | `F1490200` | | Approval 1 OpBlock | 5 | `05000000` | | Approval 2 Block | 150007 | `F7490200` | | Approval 2 OpBlock | 1 | `01000000` | | Approval 3 Block | 150012 | `FC490200` | | Approval 3 OpBlock | 2 | `02000000` | | Subject | Approved by 3 | `417070726F7665642062792033` | ``` 03F149020005000000F749020001000000FC49020002000000417070726F7665642062792033 ``` ```javascript const Coding = require('@pascalcoin-sbx/common').Coding; const Endian = require('@pascalcoin-sbx/common').Endian; const AccountNumber = require('@pascalcoin-sbx/common').Types.AccountNumber; const CompositeType = Coding.CompositeType; class BlockAndOp extends CompositeType { constructor() { super('block_and_op'); this.addSubType(new Coding.Core.Int32('block', true, Endian.LITTLE_ENDIAN)); this.addSubType(new Coding.Core.Int32('opblock', true, Endian.LITTLE_ENDIAN)); } } class FinishPayload extends CompositeType { constructor() { super('escrow_finish'); this.addSubType(new Coding.Core.Int8('approvalCount', true)); this.addSubType( new Coding.Repeating('approvals', new BlockAndOp('approval'), 6, 'approvalCount') ); this.addSubType(new Coding.Core.StringWithoutLength('subject')); } } let finish = { approvalCount: 3, approvals: [ { block: 150001, opblock: 5 }, { block: 150007, opblock: 1 }, { block: 150012, opblock: 2 }, ], subject: "Approved by 3" }; console.log(new FinishPayload().encodeToBytes(finish).toHex()); // 03F149020005000000F749020001000000FC49020002000000417070726F7665642062792033 ```