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
159 changes: 159 additions & 0 deletions ARCs/arc-0063.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
arc: 63
title: Lsig Plug-In Signer for Msig Vault Opt-In
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: Interface
created: 2024-07-16
---

## Abstract

This ARC proposes a method for creating a delegated multisig-account control only by one account and a Logic signature.

## Motivation

The motivation behind this ARC is to extend algorand account feature by enabling third-party "Plug-Ins" using a combinaison 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 deterministic wallet and rekeying mechanisms.

## 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. **Lsig Plug-In**: Provided by a third party.
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved
2. **Plug-In Signer**: Created by concatenating the owner address and the Lsig address, then signed with owner secret key to generate the plug-in signer account.
3. **1/3 Msig Account**: Comprises owner address, the Lsig address, and the plug-in signer.

### Steps

We will use the following lsig plug-in for our illustrating purposes:

```python

def opt_in_logic_sig():
return And(
Txn.type_enum() == TxnType.AssetTransfer,
Txn.asset_amount() == Int(0),
Txn.rekey_to() == Global.zero_address(),
Txn.fee() == Global.min_txn_fee()
)
teal_program = compileTeal(opt_in_logic_sig(), Mode.Signature, version=10)
compiled_program = client.compile(teal_program)
program = base64.b64decode(compiled_program["result"])
lsig = transaction.LogicSigAccount(program)

```

1. **Generate Plug-In Signer**:
- Concatenate owner address and the Lsig address.
- Sign the concatenated byte string with owner secret key.
- Use the resulting signature to generate the plug-in signer account.

```python
def generate_32bytes_from_addresses(addr1, addr2, sk):
combined = addr1 + addr2
combined_signed = util.sign_bytes(combined.encode(), sk)
hash_digest = hashlib.sha256(combined_signed.encode()).digest()
seed = hash_digest[:32]
sk = SigningKey(seed,encoder=nacl.encoding.RawEncoder)
vk = sk.verify_key
a = encoding.encode_address(vk.encode())
return base64.b64encode(sk.encode() + vk.encode()).decode(), a

plug_in_sk, plug_in_addr = generate_32bytes_from_addresses(bob_addr, lsig.address(), bob_sk)
```

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/3 Msig Account**:
- Create a multi-signature account with owner address, the Lsig address, and the plug-in signer.

```python
bob_vault_msig = transaction.Multisig(1,1,[bob_addr, lsig.address(), plug_in_addr])
```

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

```json
{
"s": bob_addr,
"lsigs": lsig.address(),
"sigA": plug_in_addr,
"sigS": plug_in_public_sig,
"vault": bob_vault_msig.address(),
}
```

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

```python
ptxn = transaction.PaymentTxn(
bob_addr, sp, bob_vault_msig.address(), int(1e6), note=f"arc_63:j:{note_field}"
).sign(bob_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=bob_vault_msig.address(),
sp=sp,
receiver=bob_vault_msig.address(),
amt=0,
index=a_id,
)
lsig.lsig.msig = bob_vault_msig
lsig.lsig.msig.subsigs[2].signature = base64.b64decode(plug_in_public_sig) # signature from plug_in_public
lstx = LogicSigTransaction(optin_txn, lsig)
```

5. **Rekey Plug-In Signer**:
- Rekey the plug-in signer to the zero address to prevent any further usage.

### Rekeying Process

The plug-in signer is rekeyed to the zero address to eliminate the risk of unauthorized transactions. This ensures that the signer cannot be used post-creation, maintaining the integrity of the multi-signature setup.
SudoWeezy marked this conversation as resolved.
Show resolved Hide resolved

### Schema

![ARC Status Diagram](../assets/arc-0063/image.png)

## Rationale

The rationale for this design is to leverage third-party Lsig plug-ins. By rekeying the plug-in signer, 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.

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

[Exemple from wallet side](../assets/arc-0063/wallet_view.py)

## Security Considerations

The key security consideration is the rekeying of the plug-in signer to the zero address. This step is crucial to prevent any unauthorized use of the signer post-creation. Additionally, the inherent security of multi-signature accounts provides an added layer of protection.

## Copyright

Copyright and related rights waived via <a href="https://creativecommons.org/publicdomain/zero/1.0/">CCO</a>.
182 changes: 182 additions & 0 deletions assets/arc-0063/create_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
from algosdk import mnemonic, transaction, encoding, util, constants
from algosdk.v2client import algod, indexer
from algosdk.transaction import AssetTransferTxn, AssetCloseOutTxn
from algosdk.transaction import LogicSigTransaction
from pyteal import Txn, And, TxnType, Int,Global, compileTeal, Mode
import base64
from nacl.signing import SigningKey
import nacl.encoding
import hashlib

import json

algod_address = "http://localhost:4001" # Adjust if using a different port
algod_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
client = algod.AlgodClient(algod_token, algod_address)
indexer_address = "http://localhost:8980" # Adjust if using a different port
indexer_token = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
indexer = indexer.IndexerClient(indexer_token, indexer_address)
sp = client.suggested_params()

def generate_32bytes_from_addresses(addr1, addr2, sk):
combined = addr1 + addr2
combined_signed = util.sign_bytes(combined.encode(), sk)
hash_digest = hashlib.sha256(combined_signed.encode()).digest()
seed = hash_digest[:32]
sk = SigningKey(seed,encoder=nacl.encoding.RawEncoder)
vk = sk.verify_key
a = encoding.encode_address(vk.encode())
return base64.b64encode(sk.encode() + vk.encode()).decode(), a

def opt_in_logic_sig():
return And(
Txn.type_enum() == TxnType.AssetTransfer,
Txn.asset_amount() == Int(0),
Txn.rekey_to() == Global.zero_address(),
Txn.fee() == Global.min_txn_fee()
)
teal_program = compileTeal(opt_in_logic_sig(), Mode.Signature, version=10)
compiled_program = client.compile(teal_program)
program = base64.b64decode(compiled_program["result"])
lsig = transaction.LogicSigAccount(program)

add = [
{
"address": "45UO5ZGAAV3VSUFWPY72UITVNSWKLSYJBBALU2O56E32QQYHXHCI5D2PDA",
"mnemonic": "dumb pencil plastic isolate butter ribbon glide tragic pulse empty grape double glass stadium disorder riot agent donkey city weird shadow bubble ladder absent kidney",
},
{
"address": "6QZBRTHUT4P4D26HBW7NSJJ26P3WV4NWXLBF7AB5TVDVJFXLFLN6RMZQKI",
"mnemonic": "sense gate people glare window bright betray tiny group subject blast gasp cargo safe play news inhale evolve luggage coil biology wide custom absorb trust",
}
]

bob_sk, bob_addr = mnemonic.to_private_key(add[0]["mnemonic"]), add[0]["address"]
alice_sk, alice_addr = mnemonic.to_private_key(add[1]["mnemonic"]), add[1]["address"]

#******************** Plug_IN_Signer Account Generation ****************************#
plug_in_sk, plug_in_addr = generate_32bytes_from_addresses(bob_addr, lsig.address(), bob_sk)

#******************** Plug_IN_Signer Public Signature ****************************#
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()
#******************** REKEY PLUG_IN TO _ ****************************# #Alice for testing purposes but should be 0 address
ptxn = transaction.PaymentTxn(
bob_addr, sp, plug_in_addr, int(1e5 + 1e3)
).sign(bob_sk)
txid = client.send_transaction(ptxn)
results = transaction.wait_for_confirmation(client, txid, 4)
print(f"Result confirmed in round: {results['confirmed-round']}")

rekey_txn = transaction.PaymentTxn(
plug_in_addr, sp, plug_in_addr, 0, rekey_to=alice_addr
)
signed_rekey = rekey_txn.sign(plug_in_sk)
txid = client.send_transaction(signed_rekey)
result = transaction.wait_for_confirmation(client, txid, 4)
print(f"Rekey transaction confirmed in round {result['confirmed-round']}")



#********************* MSIG VAULT Generation **************************************#
bob_vault_msig = transaction.Multisig(1,1,[bob_addr, lsig.address(), plug_in_addr])


print("Bob vault addresses : ", bob_vault_msig.address()) #NUVYGSZCMMH65PGYGPRB63JNZMMIKT6ROK5HNM3BKVKLSNL77FTB7DKMJU
for i in bob_vault_msig.subsigs:
print("Bob vault address: ", encoding.encode_address(i.public_key), base64.b64encode(i.public_key))


# print(int(client.account_info(bob_vault_msig.address())["amount"]))

if int(client.account_info(bob_vault_msig.address())["amount"]) < 2e5:
#********************FUND BOB MSIG VAULT****************************#
note_field = json.dumps({
"s": bob_addr,
"lsigs": lsig.address(),
"sigA": plug_in_addr,
"sigS": plug_in_public_sig,
"vault": bob_vault_msig.address(),
})
ptxn = transaction.PaymentTxn(
bob_addr, sp, bob_vault_msig.address(), int(1e6), note=f"arc_63:j:{note_field}"
).sign(bob_sk)
txid = client.send_transaction(ptxn)
results = transaction.wait_for_confirmation(client, txid, 4)
print(f"Result confirmed in round: {results['confirmed-round']}")

alice_info = client.account_info(alice_addr)
if 'assets' in alice_info and (len(alice_info['assets']) > 0):
a_id = client.account_info(alice_addr)['assets'][0]['asset-id']
else:
#******************** ALICE CREATE ASA ****************************#
print("Alice Create ASA")
actxn = transaction.AssetConfigTxn( sender=alice_addr, sp=sp, default_frozen=False, unit_name="rug2", asset_name="2 Really Useful Gift", manager=alice_addr, reserve=alice_addr, freeze=alice_addr, clawback=alice_addr, url="https://path/to/my/asset/details", total=10, decimals=0, )
sactxn = actxn.sign(alice_sk)
tx_id = client.send_transaction(sactxn)
print(f"Sent asset create transaction with txid: {tx_id}")
# Wait for the transaction to be confirmed
results = transaction.wait_for_confirmation(client, tx_id, 4)
a_id = results['asset-index']
print(f"Result confirmed in round: {results['confirmed-round']} ASA ID : {results['asset-index']}")
print(f'Alice created 10 ASA: {a_id}')


#******************** MSIG VAULT OPT IN ****************************#
optin_txn = AssetTransferTxn(
sender=bob_vault_msig.address(),
sp=sp,
receiver=bob_vault_msig.address(),
amt=0,
index=a_id,
)
lsig.lsig.msig = bob_vault_msig
lsig.lsig.msig.subsigs[2].signature = base64.b64decode(plug_in_public_sig) # signature from plug_in_public
lstx = LogicSigTransaction(optin_txn, lsig)



optin_txid = client.send_transaction(lstx)


print(f"Sent Msig Vault Opt-in with txid: {optin_txid}")
# Wait for the transaction to be confirmed
results = transaction.wait_for_confirmation(client, optin_txid, 4)
print(f"Result confirmed in round: {results['confirmed-round']}")
print(" #********************Opt Out ASA***************************#")
bob_vault_msig = transaction.Multisig(
1,
1,
[bob_addr, transaction.LogicSigAccount(program).address(), plug_in_addr]
)
print(transaction.LogicSigAccount(program).address())

optout_txn = AssetCloseOutTxn(
sender=bob_vault_msig.address(),
sp=sp,
receiver=bob_vault_msig.address(),
index=a_id
)

msig_txn = transaction.MultisigTransaction(optout_txn, bob_vault_msig)
msig_txn.sign(bob_sk)
optout_txn = client.send_transaction(msig_txn)

print(f"Sent Msig Vault Opt-out with txid: {optout_txn}")
# Wait for the transaction to be confirmed
results = transaction.wait_for_confirmation(client, optout_txn, 4)
print(f"Result confirmed in round: {results['confirmed-round']}")
print(f"{bob_vault_msig.address()} Opted - Out {a_id}")

#********************REKEY BACK************************#
rekey_back_txn = transaction.PaymentTxn(
plug_in_addr, sp, plug_in_addr, 0, rekey_to=plug_in_addr, close_remainder_to=bob_addr
)
signed_rekey_back = rekey_back_txn.sign(alice_sk)
txid = client.send_transaction(signed_rekey_back)
result = transaction.wait_for_confirmation(client, txid, 4)
print(f"Rekey back transaction confirmed in round {result['confirmed-round']}")
Binary file added assets/arc-0063/image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading