Skip to content
261 changes: 261 additions & 0 deletions dips/dip-169.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
---
dip: 169
title: Multi-Agent Transactions
authors: Emma Zhong, Tim Zakian, Sam Blackshear
status: Draft
type: Standard
created: 04/05/2021
issue: https://github.com/diem/dip/issues/169
---


# Summary

This DIP describes a new scheme of user transaction: multi-agent transactions.
Comment thread
emmazzz marked this conversation as resolved.
Outdated

# Abstract

Currently in the Diem Framework, a transaction acts on behalf of a single on-chain account. However, there is no mechanism for multiple on-chain accounts to agree on a single atomic transaction. This DIP presents a new scheme of transactions--multi-agent transactions--which act on behalf of multiple on-chain accounts. Multi-agent transactions leverage Move’s [_`signer`_](https://developers.diem.com/docs/move/move-signer/) type to allow essentially any arbitrary atomic actions in one transaction involving multiple on-chain accounts.
Comment thread
emmazzz marked this conversation as resolved.
Outdated

## Terminology:

* Primary signer: This is the account that the transaction is sent from. This account’s sequence number is incremented, and is the account that pays gas. There is precisely one of these for every transaction.
Comment thread
emmazzz marked this conversation as resolved.
Outdated
* Secondary signer: This is any account that participates in a multi-agent transaction that isn’t the primary sender. There can be 0-N of these. The 0 case is a normal transaction today.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did we put a max on N in the code? We do this for multisig and should probably do the same + specify it here. Perhaps we can use the same limit as multisig so that an attacker has the same number of "free" signature checks in each option?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't put a max on N here. Yes we should put a limit same as for multisig. Will work on that.


# Motivation/Use Cases
Comment thread
emmazzz marked this conversation as resolved.

## Minting Directly to VASPs

Today in the Diem Framework, we need two transactions in order to mint money through designated dealers to VASPs. The first transaction is a tiered-mint transaction sent by the treasury compliance account to mint money to a designated dealer’s account. The second transaction transfers the minted funds from the designed dealer’s account into the VASP’s account. This procedure requires two transactions due to the restriction that each transaction can only have one signer argument. In the multi-agent scheme, this restriction no longer exists and we can perform both steps in a single atomic transaction (see code sample below). This atomicity implies that designated dealers no longer need custody services for the money they temporarily hold in their accounts, making it more affordable and much quicker to operate as a designated dealer. This should make it much easier for more DDs to onboard in the Diem ecosystem, providing more options for VASPs. It is highly likely that in the future, DDs primarily mint directly on requests from VASPs.
Comment thread
emmazzz marked this conversation as resolved.
Outdated

```rust
fun mint<Coin>(
tc: signer,
dd: signer,
vasp_addr: address,
amount: u64,
tier_index: u64
) {
// First, TC mints to DD.
tiered_mint<Coin>(
tc, address_of(dd), amount, tier_index
);
Comment thread
emmazzz marked this conversation as resolved.
Outdated

// Then, DD distributes funds to VASP.
pay<Coin>(
dd, vasp_addr, amount
);
}
```



## Atomic Swaps

In order to do a currency exchange in the current scheme between two on-chain entities Alice and Bob, we need two transactions and possibly an escrow. The time gap between the two transactions can lead to potential problems such as resource lockup. With the new multi-agent transaction scheme, we only need one atomic transaction signed by both Alice and Bob, in which payments are sent in both directions (see the code sample below). In this case, both Alice and Bob have the same control over the transaction contents including exchange rates and expiration time.


```rust
// alice and bob agree on the values of amount_usd and amount_eur
// off-chain.
fun exchange(
alice: signer, bob: signer,
amount_usd: u64, amount_eur: u64
) {
// First, Alice pays Bob in currency USD.
pay<USD>(alice, address_of(bob), amount_usd);

// Then, Bob pays Alice in currency EUR.
// Previously, this line has to be a separate transaction.
pay<EUR>(bob, address_of(alice), amount_eur);
}
```



## Actual Dual-attested Transactions
Comment thread
emmazzz marked this conversation as resolved.
Outdated

Currently, we execute a [_dual-attested payment_](https://dip.diem.com/dip-1/) transaction by including a signature of the payee as an argument to the transaction, and checking the validity of the signature on-chain if the payment is between two different VASPs and the amount is over a certain threshold. In order to accomplish that, we have to run some fairly complex code to reconstruct and verify the signature ad-hoc on-chain. With the new scheme, we can simply require that the payee signs the entire transaction as well, and verify the signatures of both the payer and payee while validating the transaction. The code samples below show the before and after of what such a transaction might look like.
Comment thread
emmazzz marked this conversation as resolved.
Outdated

Some benefits brought by the multi-agent scheme in this scenario are:

* Extensibility: In the current scheme, our dual attestation protocol is very specific to travel rule payments. And extending this protocol to dual attestation on a different type of action would require another ad-hoc data format and signature scheme. The multi-agent transaction scheme generalizes this by allowing dual attestation on any action encodable by a Move script without having to define new data formats or signature schemes.
Comment thread
emmazzz marked this conversation as resolved.
Outdated
* Security: Multi-agent scheme closes two security loopholes of our current protocol:
* Replayability: Since the payee only signs over the payer and the amount of the payment, today the payer can replay the transaction using old payee approvals. In the new scheme, this wouldn’t be possible since the payee has to sign over the sequence number of the transaction as well, disallowing any replay of the transaction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be a bit cautious with this statement. Isn't there protection against this? Isn't the receiver signing the full transaction which includes a unique sequence number, and if replayed by the sender, it will be rejected?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no protection against this. The recipient does not sign the full transaction, only an (amount, sender_address, metdata) approval message. This approval message can be replayed an arbitrary number of times.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. This is not an issue with single-agent transactions, it is a problem with DIP-1 which could be fixed there, right? I would prefer to tone this down and mention the issue with DIP-1.

Here, how about:
The Multi-agent natively leads to robust and secure dual-attestation protocols, since both parties sign the entire transaction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with mentioning the motivation in more general terms as you suggest, although I do think a specific example helps.

As an aside, it's not easy to fix the problem in DIP1 without this feature + CRSN's. If the approval message signs (e.g.) the payer's sequence number, the payer has difficulty sending other transactions concurrently with a TR transaction without invaliding the payee's approval. And it's not easy to sign over the currency without this feature because currency is a Move type, and Move doesn't allow you to reflect a type into a value that can then been signed.

* Currency: Today’s protocol does not let the payee commit to the currency type so the payer can reuse a payee approval for payment in one currency for payment in another with the same amount. Again, since the payee has to sign over the entire transaction including the currency type, this scenario would be impossible with the multi-agent scheme.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approval message does not contain the currency, so this is indeed also a problem with the current scheme.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

* Earlier failure: Currently a bad signature in a travel rule transaction will be caught at execution time, and the transaction will abort. In the new scheme, the signature is checked at validation time and the transaction will be discarded instead.

```rust
/// Before:
fun dual_attested_payment<Coin>(
payer: signer, payee: address, amount: u64,
metadata: vector<u8>,
metadata_signature: vector<u8>
){
let msg = message(payer, amount, metadata);
verify_signature(compliance_key(payee), msg);
pay<Coin>(payer, payee, amount, metadata);
}

/// After:
fun dual_attested_payment<Coin>(
payer: signer, payee: signer, amount: u64,
metadata: vector<u8>
){
pay<Coin>(payer, address_of(payee), amount, metadata);
}
```



## Arbitrary Atomic Actions in One Transaction

Besides the use cases mentioned above, our new multi-agent transaction scheme allows the sender to encode any arbitrary combination of actions in a single atomic transaction. This added expressiveness opens up a lot more possibilities in the type of scenarios that we can implement. Some examples include [_delivery versus payment_](https://www.investopedia.com/terms/d/dvp.asp) (a crucial feature for securities settlement) and atomic administrative approval of sensitive actions (e.g., account creation).
Comment thread
emmazzz marked this conversation as resolved.
Outdated




# Code Changes to Support Multi-agent

## DiemVM Code Changes
Comment thread
emmazzz marked this conversation as resolved.
Outdated

### Add New Enum `RawTransactionWithData`

In order to preserve backward compatibility, we can’t add more fields representing secondary signers to `RawTransaction` struct. Thus we add the following additional enum `RawTransactionWithData`, which the sender and secondary signers will sign over to authenticate the transaction.
Comment thread
emmazzz marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In order to preserve backward compatibility, we can’t add more fields representing secondary signers to `RawTransaction` struct. Thus we add the following additional enum `RawTransactionWithData`, which the sender and secondary signers will sign over to authenticate the transaction.
To preserve backward compatibility, we can’t add more fields representing secondary signers to `RawTransaction` struct. Thus we add the following additional enum `RawTransactionWithData`, which the primary and secondary signers will sign over to authenticate the transaction.



```rust
pub enum RawTransactionWithData {
MultiAgent {
raw_txn: RawTransaction,
secondary_signer_addresses: Vec<AccountAddress>,
},
}
```



### Add New Enum `AccountAuthenticator`

Previously each transaction can only have one signer, which is the sender of the transaction. In the multi-agent scheme, with the ability to have multiple signers, the transaction can potentially have multiple authenticators from different accounts and of different schemes. An `AccountAuthenticator` serves as the authenticator for one account. And a `TransactionAuthenticator` can contain multiple `AccountAuthenticator`s, as shown in the next subsection.
Comment thread
emmazzz marked this conversation as resolved.
Outdated

Notice that `AccountAuthenticator` has a multi-signature variant `MultiEd25519`. `MultiEd25519` is a signature scheme an account uses to generate signatures. This is different from multi-agent, which is a new type of transaction.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that an account using MultiEd25519 can't be part of a MultiAgentTransaction?

I don't know much about which accounts use MultiEd25519, though. Is there a reference on that?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1. I think it would be useful to explain how to reconcile MultiAgent and MultiEd25519 if one is a sig scheme and another a txn scheme and why they are both variants of TransactionAuthenticator?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the difference is that MultiAgent does not require to combine signatures off-chain, and the combined signature is a single signature corresponding to a single account

@sblackshear sblackshear May 6, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, the addition of the multi-agent feature wouldn't give an attacker any more DDOS power, but I think we will need to tweak the rules/code a bit to ensure that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A check has been added to allow no more than 32 signatures. DIP has also been updated to talk about this check in section Additional Checks During Transaction Validation.


```rust
pub enum AccountAuthenticator {
/// Single signature
Ed25519 {
public_key: Ed25519PublicKey,
signature: Ed25519Signature,
},
/// K-of-N multisignature
MultiEd25519 {
public_key: MultiEd25519PublicKey,
signature: MultiEd25519Signature,
},
}
```



### Add a New Variant to `TransactionAuthenticator`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a New Variant or a new field?

@sblackshear sblackshear May 6, 2021

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Variant" is the right technical term for one of the potential values of a sum type like TransactionAuthenticator.


We have added one new variant to `TransactionAuthenticator` for the multi-agent transaction scheme. In the multi-agent scheme, the sender and all the secondary signers have to sign over `RawTransactionWithData::MultiAgent{ raw_txn, secondary_signer_addresses }`, to make sure that all parties agree to transact with each other.


```rust
pub enum TransactionAuthenticator {
/// Single signature
Ed25519 {
public_key: Ed25519PublicKey,
signature: Ed25519Signature,
},
/// K-of-N multisignature
MultiEd25519 {
public_key: MultiEd25519PublicKey,
signature: MultiEd25519Signature,
},
/// Multi-agent transaction.
MultiAgent {
sender: AccountAuthenticator,
Comment thread
emmazzz marked this conversation as resolved.
Outdated
secondary_signer_addresses: Vec<AccountAddress>,
secondary_signers: Vec<AccountAuthenticator>,
},
}
```



### Additional Checks During Transaction Validation

In addition to signature checking, we also perform the following additional checks on signed multi-agent transactions during transaction validation:

* The number of secondary authenticators in the signed transaction is the same as the number of secondary signer addresses that all parties have signed over.
* There are no duplicates in the signers. In other words, sender and all the secondary signers have distinct account addresses.
Comment thread
emmazzz marked this conversation as resolved.
Outdated




## Diem Framework Changes
Comment thread
emmazzz marked this conversation as resolved.

A new prologue called `multi_agent_script_prologue` is added to `DiemAccount.move` to validate multi-agent transactions.
```rust
fun multi_agent_script_prologue<Token: store>(
sender: signer,
txn_sequence_number: u64,
txn_sender_public_key: vector<u8>,
secondary_signer_addresses: vector<address>,
secondary_signer_public_key_hashes: vector<vector<u8>>,
txn_gas_price: u64,
txn_max_gas_units: u64,
txn_expiration_time: u64,
chain_id: u8,
)
```

Following are the additional checks it performs.
Comment thread
emmazzz marked this conversation as resolved.
Outdated


### Account Existence Checking in Prologue

Today we check in the prologue that the sender of the transaction has a `DiemAccount` resource under their address. With the ability to have multiple signers, we need to check that all of them have `DiemAccount` resources during the prologue.

```rust
let i = 0;
while (i < num_secondary_signers) {
let secondary_address = *Vector::borrow(&secondary_signer_addresses, i);
assert(
exists_at(secondary_address),
Errors::invalid_argument(PROLOGUE_EACCOUNT_DNE)
);
i = i + 1;
};
```



### Authentication Key Checking in Prologue

Currently in the prologue, we check that the hash of the sender’s public key is equal to the authentication key that the sender has on chain. In the new scheme, we will need to perform the same check for public keys of all the secondary signers as well. In the `multi_agent_script_prologue`, we add a loop going through the two vectors containing addresses and public key hashes to perform the aforementioned check.


```rust
let i = 0;
while (i < num_secondary_signers) {
let secondary_address = *Vector::borrow(&secondary_signer_addresses, i);
let signer_account = borrow_global<DiemAccount>(secondary_address);
let signer_public_key_hash = *Vector::borrow(&secondary_signer_public_key_hashes, i);
assert(
signer_public_key_hash == *&signer_account.authentication_key,
Errors::invalid_argument(PROLOGUE_EINVALID_ACCOUNT_AUTH_KEY),
);
i = i + 1;
};
```

# Feature Gating

Since there is no application of multi-agent transaction scheme in Diem yet, this feature is gated behind `DiemVersion = 10`. Multi-agent transactions are rejected when the Diem version is less than 10.
Comment thread
emmazzz marked this conversation as resolved.
Outdated