Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ARC-63 Delegated multisig-account #303

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
322 changes: 322 additions & 0 deletions ARCs/arc-0063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,322 @@
---
arc: 63
title: Lsig Plug-In Signer for Msig Vault
description: Delegated multisig-account controlled by one account
author: Stéphane BARROSO (@SudoWeezy)
discussions-to: https://github.com/algorandfoundation/ARCs/issues/303
status: Draft
type: Standards Track
category: ARC
created: 2024-07-16
---

## Abstract

This ARC proposes a method for creating a delegated multisig account controlled by one account and a Logic Signature (Lsig).

## Motivation

The motivation behind this ARC is to extend Algorand account features by enabling third-party "Plug-Ins" using a combination of delegated Lsig and Multi-Signature accounts, which act as vaults. This approach allows anyone to sign the Lsig for the vault, while maintaining security and control through a classic algorand account.

## Specification

The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in <a href = "https://www.ietf.org/rfc/rfc2119.txt">RFC-2119</a>

### Components

1. **Owner Account**: Vault's Owner
1. **Lsig Plug-In**: Provided by a third party.
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved
1. **Plug-In Signer**: Created by generating a new key pair
1. **1/2 Msig Account**: Comprises the owner's address, and the plug-in signer.

### Implementation 1

We will use the following Logic plug-in for illustrative purposes:

**DO NOT USE IN PRODUCTION**

```python
teal_program = """
#pragma version 10
txn TypeEnum
pushint 4
==
txn AssetAmount
pushint 0
==
&&
txn RekeyTo
global ZeroAddress
==

Choose a reason for hiding this comment

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

Suggested change
==
==
&&
txn AssetCloseTo
global ZeroAddress
==

Copy link
Collaborator Author

@SudoWeezy SudoWeezy Oct 1, 2024

Choose a reason for hiding this comment

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

Good catch, to be even more strict, we could also add this:

==
global GenesisHash
byte base64(wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=)
==
&&
==
&&
txn FreezeAssetAccount
global ZeroAddress
==

&&
txn Fee
global MinTxnFee
==
&&
return
"""
compiled_program = client.compile(teal_program)
program = base64.b64decode(compiled_program["result"])
lsig = transaction.LogicSigAccount(program)
```

> This give opt-in control over the signer

1. **Generate Plug-In Signer**:
- Generate a random new account that we will only use once.

```python
plug_in_sk, plug_in_addr = account.generate_account()
```

2. **Sign Lsig with Plug-In Signer**:
- Sign the Lsig using the plug-in signer.
- Publish the public signature on the indexer.

```python
public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair(base64.b64decode(plug_in_sk)[: constants.key_len_bytes])
message = constants.logic_prefix + program
raw_signed = nacl.bindings.crypto_sign(message, secret_key)
crypto_sign_BYTES = nacl.bindings.crypto_sign_BYTES
signature = nacl.encoding.RawEncoder.encode(raw_signed[:crypto_sign_BYTES])
plug_in_public_sig = base64.b64encode(signature).decode()
Copy link
Contributor

Choose a reason for hiding this comment

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

This is creating a signature based on the logic signature program right? Aka producing a delegate signature for the newly generated account from step 1.

Is it worth using the Algorand SDK (LogicSigAccount(program).sign(secret_key)) in this example instead of directly using nacl? I suspect it would be clearer to our demographic.

Copy link
Collaborator Author

@SudoWeezy SudoWeezy Jul 20, 2024

Choose a reason for hiding this comment

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

From my understanding, this (LogicSigAccount(program).sign(secret_key)) method does not return the public signature def sign(self, private_key: str) -> None:, it will instead store it inside an lsig object like this:
lsig = transaction.LogicSigAccount(program).sign(secret_key)

But to extract the public signature, we shoud do the following:
plug_in_public_sig = lsig.lsig.sig
It works but it seems a bit more "magical"

But you are right, I will add it for reference, so it is easier to follow what I am doing here.

```

3. **Create 1/2 Msig Account**:
- Create a multi-signature account with owner address, the Lsig address, and the plug-in signer.
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved

```python
owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr])
```

- Add a transaction note to the transaction to help third party to retrieve signer and vault information.

```json
{
"pk": plug_in_addr,
"sk": plug_in_public_sig,
"lsig": lsig.address(),
}
```

- Prefix the note following the [ARC-2](./arc-0002.md) standard. `arc63:j`

```python
ptxn = transaction.PaymentTxn(
owner_addr, sp, owner_vault_msig.address(), int(1e6), note=f"arc_63:j{note_field}"
).sign(owner_sk)
```

4. **Opt-In to Msig Vault**:
- Anyone can opt-in to the Msig vault using the plug-in signer’s public address and the published signature.

```python
optin_txn = AssetTransferTxn(
sender=owner_vault_msig.address(),
sp=sp,
receiver=owner_vault_msig.address(),
amt=0,
index=a_id,
)
lsig.lsig.msig = owner_vault_msig
lsig.append_to_multisig(plug_in_sk) # signature from plug_in_public
lstx = LogicSigTransaction(optin_txn, lsig)
```

### Implementation 2

We will use the following Lsig plug-in for our illustrative purposes:

**DO NOT USE IN PRODUCTION**

```python
teal_program = f"""
#pragma version 10
txn TypeEnum
int appl
==
txn ApplicationID
int {app_client.app_id}
==
&&
txn RekeyTo
global ZeroAddress
==
&&
txn Fee
global MinTxnFee
==
&&
return
"""
compiled_program = client.compile(teal_program)
program = base64.b64decode(compiled_program["result"])
lsig = transaction.LogicSigAccount(program)
```

> This allow the application with the id `app_client.app_id` to control the signer

To be consistant with the previous example, we will use a similar opt-in process.

```python
class SmartApp(ARC4Contract):
def __init__(self) -> None:
self.db = BoxMap(Account, String, key_prefix="")

@abimethod()
def opt_in(self, id: UInt64, account: Account) -> None:
itxn.AssetTransfer(
asset_amount=0,
xfer_asset=id,
sender=account,
asset_receiver=account,
fee=1000,
).submit()

@abimethod()
def set_public_sig(self, account: Account, sig: String) -> bool:
self.db[account] = sig
return self.db[account] == sig

@abimethod(readonly=True)
def get_public_sig(self, account: Account) -> String:
return self.db[account]
```

1. **Generate Plug-In Signer**:
- Generate a random new account that we will only use once.

```python
plug_in_sk, plug_in_addr = account.generate_account()
```

2. **Sign Lsig with Plug-In Signer**:
- Sign the Lsig using the plug-in signer.

```python
public_key, secret_key = nacl.bindings.crypto_sign_seed_keypair(base64.b64decode(plug_in_sk)[: constants.key_len_bytes])
message = constants.logic_prefix + program
raw_signed = nacl.bindings.crypto_sign(message, secret_key)
crypto_sign_BYTES = nacl.bindings.crypto_sign_BYTES
signature = nacl.encoding.RawEncoder.encode(raw_signed[:crypto_sign_BYTES])
plug_in_public_sig = base64.b64encode(signature).decode()
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved
```

3. **Create 1/2 Msig Account**:
- Create a multi-signature account with owner address, the Lsig address, and the plug-in signer.
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved

```python
owner_vault_msig = transaction.Multisig(1,1,[owner_addr, plug_in_addr])
```

- Publish the public signature by using set_public_sig to the app.

```python
response = app_client.set_public_sig(
account=owner_addr,
sig=plug_in_public_sig,
transaction_parameters=algokit_utils.TransactionParameters(
boxes=[(app_client.app_id, encoding.decode_address(owner_addr))],
accounts=[owner_addr]
)
)
```

- Get the transaction set_public_sig from the app.

```python
response = app_client.get_public_sig(
account=owner_addr,
transaction_parameters=algokit_utils.TransactionParameters(
boxes=[(app_client.app_id, encoding.decode_address(owner_addr))]
),
)
```

4. **App in control of Msig Vault**:
- Anyone can now call the app to opt-in any asset to the Msig vault using the plug-in signer’s public address and the published signature.
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure I understand this section. The msig account hasn't been rekeyed to the application, so how does the application control the multisig vault at this point? All it has is a list of addresses to signatures, which haven't been verified to be accurate, presumably because it's an early design? Or have I misunderstood it, and this application is just there to provide a mapping service between address and signature?

Copy link
Collaborator Author

@SudoWeezy SudoWeezy Jul 20, 2024

Choose a reason for hiding this comment

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

With the signature plug_in_public_sig of the plug_in_addr that is now stored on the app thanks to the set_public_sig() method, anyone can get this value by looking at the box/ calling the get_public_sig() method.

{
    ...
    inner-txns:[...],
    signature:{
    logicsig:{
    logic:"CjEQgQYSMRiB7QcSEDEgMgMSEDEBMgASEEM=",
    multisig-signature:{
        version:1,
        threshold:1,
        subsignature:[
                {...},
                {
                    public-key:"24RB9iNUHfFDelIYDnw9URA2Pf6TYz8vg/vom9sUwAI=",
                    signature:"iPu354yel81lxlJTlFPG34AxDVR5Bc1MLL1zRwWwK7zM2p4Ehkf5ktOH2H67avgO31a0v8ejr/DL328jNAj4BA=="
                }
            ]
        }
    }
}

image

As plug_in_addr signed the teal_program, it is enough to allow an inner transaction (binded into a LogicSigTransaction) doing operations on the msig account.
To recontruct a multisignature to execute the application call an opt in on behalf of the user, we can just execute the code bellow.

composer.opt_in(
    id=a_id,
    account=app_client.app_address,
    transaction_parameters=algokit_utils.TransactionParameters(
        foreign_assets=[a_id], signer=app_client.signer,
        sender=owner_vault_msig.address()
    ),
)
opt_in_txn = composer.atc.txn_list[0].txn

lsig.lsig.msig = owner_vault_msig
lsig.lsig.msig.subsigs[1].signature = base64.b64decode(plug_in_public_sig)
lstx = transaction.LogicSigTransaction(opt_in_txn, lsig)


```python
composer.opt_in(
id=a_id,
account=app_client.app_address,
transaction_parameters=algokit_utils.TransactionParameters(
foreign_assets=[a_id], signer=app_client.signer,
sender=owner_vault_msig.address()
),
)
opt_in_txn = composer.atc.txn_list[0].txn

lsig.lsig.msig = owner_vault_msig
lsig.lsig.msig.subsigs[1].signature = base64.b64decode(plug_in_public_sig)
lstx = transaction.LogicSigTransaction(opt_in_txn, lsig)
```

### Diagram

```mermaid
graph TD
subgraph signer
PKS[PK]
SKS[SK]
PKS ~~~ SKS
end
subgraph msigVault
SKVS[Public Signature]
SKVA[Public Address]
PKV[ownerPK]
PKV ~~~ SKVA
PKV ~~~ SKVS
end
subgraph owner
PKO[PK]
end

ta[[Throwaway Account]]

lsig((Lsig Plug In))

sign((Sign))

ta --> signer
PKO --> PKV
SKS --> sign
lsig --- sign
sign --> SKVS
PKS --> SKVA
msigVault ~~~ lsig
```

## Rationale

The rationale for this design is to leverage third-party Lsig plug-ins. By note storing the plug-in signer private key, we mitigate risks associated with its misuse, while the multi-signature account setup ensures controlled access and flexibility in asset management.

## Backwards Compatibility

This ARC introduces no backward incompatibilities. It builds upon existing Algorand functionalities, ensuring seamless integration with current systems.

## Reference Implementation

An example implementation in Python is provided, demonstrating the creation of a plug-in signer, signing an Lsig, and opting into a multi-signature vault.

### Only with Lsig

[Create_Opt_in_Plug_in](../assets/arc-0063/create_plugin.py)

### Delegating the Lsig to an App

[Deploy config](../assets/arc-0063/deploy_config.py)
[Contract](../assets/arc-0063/contract.py)

> This need to be run with Algokit

## Security Considerations

Even if the plug-in signer is rekeyed, the private key can still sign new lsigs, which is why the private key should not be accessible by anyone after the signature.
This step is crucial to prevent any unauthorized use of the signer post-creation.

If the Multi-signature vault accounts is rekeyd to any other account (Msig or traditional), it will keep the same public address and will not be delegated anymore.

## Copyright

Copyright and related rights waived via <a href="https://creativecommons.org/publicdomain/zero/1.0/">CCO</a>.
26 changes: 26 additions & 0 deletions assets/arc-0063/contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from algopy import ARC4Contract, Account, UInt64, itxn, BoxMap
from algopy.arc4 import abimethod, String


class SmartApp(ARC4Contract):
def __init__(self) -> None:
self.db = BoxMap(Account, String, key_prefix="")

@abimethod()
def opt_in(self, id: UInt64, account: Account) -> None:
itxn.AssetTransfer(
asset_amount=0,
xfer_asset=id,
sender=account,
asset_receiver=account,
fee=1000,
).submit()

@abimethod()
def set_public_sig(self, account: Account, sig: String) -> bool:
self.db[account] = sig
return self.db[account] == sig

@abimethod(readonly=True)
def get_public_sig(self, account: Account) -> String:
return self.db[account]
Loading
Loading