-
Notifications
You must be signed in to change notification settings - Fork 120
ARC-63 Delegated multisig-account #303
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
base: main
Are you sure you want to change the base?
Changes from 1 commit
e771398
4d08e6e
0cfc5e6
921511b
ae05b6c
0c6dc12
5b14218
1e1fd0c
a26ca7a
16418d8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
| 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() | ||
|
||
| ``` | ||
|
|
||
| 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.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| ### Schema | ||
|
|
||
|  | ||
|
|
||
| ## 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>. | ||
| 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']}") |
Uh oh!
There was an error while loading. Please reload this page.