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

Add multisig Python SDK tooling, tutorial #6356

Closed
wants to merge 57 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
b2d4662
Add partial multisig tutorial, Python SDK tooling
alnoki Jan 27, 2023
34bf867
Made direct edits to new first multisig tutorial, fix links along the…
clay-aptos Jan 27, 2023
3d54b05
Fix more links
clay-aptos Jan 27, 2023
ec62363
Fix links in index to work in GitHub
clay-aptos Jan 27, 2023
37b6047
Add transfer support to multisig Python tutorial
alnoki Jan 28, 2023
b50b870
Add key rotation tutorial up until failed proof
alnoki Jan 29, 2023
3c36b3f
Add RotationProofChallenge tooling, tutorial
alnoki Jan 31, 2023
a49b332
Add upgrade and govern package, publishing example
alnoki Jan 31, 2023
7e54762
Correct erroneous hyphenation in doc comment
alnoki Jan 31, 2023
5bc4a5b
Add package upgrade, Move script tutorial examples
alnoki Jan 31, 2023
393d528
Update Python SDK version minor
alnoki Jan 31, 2023
b06de58
Fix misspelling, make other tiny edits
clay-aptos Feb 1, 2023
070115a
Add link to move-examples/upgrade_and_govern directory
clay-aptos Feb 1, 2023
9be2f0c
Add missing markdown link brace
alnoki Feb 1, 2023
dc3da83
Add password-protected keystorage, begin CLI
alnoki Feb 2, 2023
94b9642
Implement private key encrypt/decrypt CLI tooling
alnoki Feb 3, 2023
1d676b7
Add keyfile operations, abstract, clean up
alnoki Feb 3, 2023
5b8a672
Add multisig metadata incorporation command
alnoki Feb 3, 2023
d171ec8
Run formatters, update tutorial var names/outputs
alnoki Feb 6, 2023
b3dcabc
Merge branch 'multisig-tutorial' into multisig-tool
alnoki Feb 6, 2023
e98b148
Apply Python auto-formatter
alnoki Feb 6, 2023
3329416
Add bytes to prefixed hex conversion helpers
alnoki Feb 6, 2023
868d660
Update assertion/exception feedback
alnoki Feb 6, 2023
333a75f
Abstract outfile writing logic
alnoki Feb 6, 2023
2ef4b6e
Add rotation proof challenge propose/sign logic
alnoki Feb 7, 2023
5652228
Add auth key rotation conversion logic
alnoki Feb 8, 2023
a2db767
Update command hierarchy, run scripted test
alnoki Feb 8, 2023
1265f3a
Add abstracted metafile mutation operations
alnoki Feb 9, 2023
64abd0b
Add metafile threshold subcommand logic
alnoki Feb 10, 2023
ba677c7
Add rotation transaction proposal logic
alnoki Feb 10, 2023
89ab5b6
Remove triple quotes for one-liner strings
alnoki Feb 10, 2023
66788f4
Add rotation transaction signing logic
alnoki Feb 10, 2023
d02a0f6
Abstract raw rotation transaction construction
alnoki Feb 10, 2023
ec6255f
Move expiry and chain ID to challenge proposal
alnoki Feb 10, 2023
6acd1b0
Add full rotation logic, demo scripts
alnoki Feb 11, 2023
97c5179
Standardize outfile syntax
alnoki Feb 11, 2023
1b84c28
Simplify single-signer determination
alnoki Feb 11, 2023
5a606f6
Add metafile funding, publication proposal
alnoki Feb 13, 2023
3aca9fd
Add publication building/signing
alnoki Feb 14, 2023
80e2901
Add publication execution logic
alnoki Feb 14, 2023
0e9a2cf
Abstract out metafile from multisig execution
alnoki Feb 14, 2023
12faf4a
Abstract extract/compile context manager
alnoki Feb 15, 2023
1c26076
Add script without non-signer arguments
alnoki Feb 15, 2023
241af06
Add script invocation support
alnoki Feb 15, 2023
f38d1d1
Sort functions
alnoki Feb 15, 2023
171d576
Update poetry dependencies
alnoki Feb 16, 2023
aebdb99
Modify password schema, add shell script tutorial
alnoki Feb 16, 2023
93a5703
Add metafile, authentication key rotation examples
alnoki Feb 17, 2023
4961912
Add collapsible output boxes, spellchecking
alnoki Feb 17, 2023
460be0f
Update doc comment, run formatter
alnoki Feb 17, 2023
bf1c18c
Add governance example
alnoki Feb 17, 2023
4ad05b4
Finalize demo scripting
alnoki Feb 17, 2023
dfba672
Merge branch 'main' into multisig-tool
alnoki Feb 17, 2023
6e29573
Fix discrepancies from compare vs main
alnoki Feb 17, 2023
20ac8f6
Update dictionary for formatting run
alnoki Feb 17, 2023
2e184de
Update argparse boilerplate to fix unittest
alnoki Feb 17, 2023
ed6e609
Remove artifacts in publication for size savings
alnoki Feb 21, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions developer-docs-site/docs/tutorials/first-multisig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: "Your First Multisig"
slug: "your-first-multisig"
---

# Your First Multisig

This tutorial introduces assorted [K-of-N multisigner authentication](../concepts/accounts#multisigner-authentication) operations, and supplements content from the following tutorials:

* [Your First Transaction](your-first-transaction)
* [Your First Coin](your-first-coin)
* [Your First Move Module](first-move-module)

:::tip
Try out the above tutorials (which include dependency installations) before moving on to multisig operations.
:::

## Step 1: Pick an SDK

This tutorial, a community contribution, was created for the [Python SDK](../sdks/python-sdk).

Other developers are invited to add support for the [TypeScript SDK](../sdks/ts-sdk/index) and the [Rust SDK](../sdks/rust-sdk)!

## Step 2: Start the example

Navigate to the Python SDK directory:

```zsh
cd <aptos-core-parent-directory>/aptos-core/ecosystem/python/sdk/examples
```

Run the [`multisig.py`](../../../ecosystem/python/sdk/examples/multisig.py) example:

```zsh
python multisig.py
```

## Step 3: Generate signers

First the example will generate single signer accounts for Alice, Bob, and Chad:

```python
:!: static/sdks/python/examples/multisig.py section_1
```

Fresh accounts are generated for each example run, but the printout should look something like the following:

```zsh
=== Account addresses ===
Alice: 0x4730cd9ecf497ead90e2fb90e9fdf734df1735815e60440b0806e30cfd3877aa
Bob: 0x682e4177c9acaecc14b5e3c1abad0f5a7caf653ab05b4093f8f80954037df446
Chad: 0x5edbae9ecf1f06498a5316f16a4545dd83ba3fcbef532f1b4ceabf3430648e76

=== Authentication keys ===
Alice: 0x4730cd9ecf497ead90e2fb90e9fdf734df1735815e60440b0806e30cfd3877aa
Bob: 0x682e4177c9acaecc14b5e3c1abad0f5a7caf653ab05b4093f8f80954037df446
Chad: 0x5edbae9ecf1f06498a5316f16a4545dd83ba3fcbef532f1b4ceabf3430648e76

=== Public keys ===
Alice: 0x44952324f5fa35bf15dc495f914864165b820a4fef39a4d2b0238168981519fe
Bob: 0x089d6e00e946af8d372ef4ef7f26e21a08cb856747014d15525180bf37f31ef5
Chad: 0x784508b54b812f89a6cb6c47010a5c389ff25316feed7f923b9e1489f7772acf
```

For each user note that the [account address](../concepts/accounts#account-address) and [authentication key](../concepts/accounts#single-signer-authentication) are identical, but the [public key](../concepts/accounts#creating-an-account) is different.

## Step 4: Generate a multisig account

Next generate a [K-of-N multisigner](../concepts/accounts#multisigner-authentication) public key and account address for a multisig account requiring 2 of the 3 signatures:

```python
:!: static/sdks/python/examples/multisig.py section_2
```

The multisig account address depends on the public keys of the single signers (hence it will be different for each example), but your printout should look something like the following:

```zsh
=== 2-of-3 Multisig account ===
Account public key: 0x1ee12500a32d35cae7788966edd224768f201f63c329c2881ceed089c113bbc4
Account address: 0x1ee12500a32d35cae7788966edd224768f201f63c329c2881ceed089c113bbc4
```

## Step 5: Fund all accounts

Next fund all accounts:

```python
:!: static/sdks/python/examples/multisig.py section_3
```

```zsh
=== Funding accounts ===
Alice's balance: 10000000
Bob's balance: 20000000
Chad's balance: 30000000
Multisig balance: 40000000
```

## Step 6: Send coins from the multisig

This transaction will send 100 octas from the multisig account to Chad's account.
Since it is a 2-of-3 multisig account, signatures are only required from 2 individual signers.

### Step 6.1 Gather individual signatures

First generate a raw transaction, signed by Alice and by Bob, but not by Chad.

```python
:!: static/sdks/python/examples/multisig.py section_4
```

Again, signatures vary for each example run:

```zsh
=== Individual signatures ===
Alice: 0x223fd617e4fb82fe211c4067356dc1d9e84c0bcc65cfdbb8da75f58c27273c55e3ed28704cb2bf8cd053ec24ac62ebc0467d3d630622f745618ad9e626c43004
Bob: 0xc8120ebeebff07b647666431ceb66fd5043635e8ccc836b99c525185b3bf5a8293de63068a06c78ce5082e5b1d4894ed0b80f34800cc3272c990d775d641740e
```

### Step 6.2 Generate a multisig authenticator

Next generate a [multisig authenticator](../guides/creating-a-signed-transaction#multisignature-transactions):


```python
:!: static/sdks/python/examples/multisig.py section_5
```
9 changes: 7 additions & 2 deletions developer-docs-site/docs/tutorials/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ If running macOS, install the following packages in the order specified to take

### [Your First Transaction](first-transaction.md)

How to [generate, submit and verify a transaction](first-transaction.md) to the Aptos blockchain.
How to [generate, submit and verify a transaction](first-transaction.md) to the Aptos blockchain.

### [Your First NFT](your-first-nft.md)

Learn the Aptos `token` interface and how to use it to [generate your first NFT](your-first-nft.md). This interface is defined in the [`token.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-token/sources/token.move) Move module.

### [Your First Move Module](first-move-module.md)

[Write your first Move module](first-move-module.md) for the Aptos blockchain.
[Write your first Move module](first-move-module.md) for the Aptos blockchain.

:::tip
Make sure to run the [Your First Transaction](first-transaction.md) tutorial before running your first Move module.
Expand All @@ -45,3 +45,8 @@ Learn how to [build your first dapp](first-dapp.md). Focuses on building the use
### [Your First Coin](first-coin.md)

Learn how to [deploy and manage a coin](first-coin.md). The `coin` interface is defined in the [`coin.move`](https://github.com/aptos-labs/aptos-core/blob/main/aptos-move/framework/aptos-framework/sources/coin.move) Move module.


### [Your First Multisig](your-first-multisig)

Learn how to perform assorted operations using [K-of-N multisigner authentication](../concepts/accounts#multisigner-authentication).
7 changes: 6 additions & 1 deletion developer-docs-site/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,12 @@ const sidebars = {
link: { type: "doc", id: "tutorials/index" },
collapsible: true,
collapsed: true,
items: ["tutorials/first-transaction", "tutorials/first-dapp", "tutorials/first-coin"],
items: [
"tutorials/first-transaction",
"tutorials/first-dapp",
"tutorials/first-coin",
"tutorials/first-multisig"
],
},
{
type: "category",
Expand Down
8 changes: 8 additions & 0 deletions ecosystem/python/sdk/aptos_sdk/account_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import hashlib

from typing import List

from . import ed25519
from .bcs import Deserializer, Serializer

Expand Down Expand Up @@ -49,6 +51,12 @@ def from_key(key: ed25519.PublicKey) -> AccountAddress:
hasher.update(key.key.encode() + b"\x00")
return AccountAddress(hasher.digest())

@staticmethod
def from_multisig_schema(
keys: List[ed25519.PublicKey], threshold: int) -> AccountAddress:
multisig_public_key = ed25519.MultiEd25519PublicKey(keys, threshold)
return AccountAddress(multisig_public_key.key.encode())

@staticmethod
def deserialize(deserializer: Deserializer) -> AccountAddress:
return AccountAddress(deserializer.fixed_bytes(AccountAddress.LENGTH))
Expand Down
19 changes: 16 additions & 3 deletions ecosystem/python/sdk/aptos_sdk/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from __future__ import annotations

import typing
from typing import List
from typing import List, Tuple

from . import ed25519
from .account_address import AccountAddress
Expand Down Expand Up @@ -146,8 +146,21 @@ def serialize(self, serializer: Serializer):


class MultiEd25519Authenticator:
def __init__(self):
raise NotImplementedError

public_key: ed25519.MultiEd25519PublicKey
signature: ed25519.MultiEd25519Signature
bitmap: bytes

def __init__(self, public_key, signature):
self.public_key = public_key
self.signature = signature
bitmap = 0
for single_signature in signature.signatures:
index = public_key.keys.index(single_signature[0])
shift = 31 - index # 32 bit positions, left to right.
bitmap = bitmap | (1 << shift)
# 4-byte big endian bitmap
self.bitmap = bitmap.to_bytes(4, 'big')

def verify(self, data: bytes) -> bool:
raise NotImplementedError
Expand Down
32 changes: 32 additions & 0 deletions ecosystem/python/sdk/aptos_sdk/ed25519.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

from __future__ import annotations

import hashlib

import unittest

from nacl.signing import SigningKey, VerifyKey
from typing import List, Tuple

from .bcs import Deserializer, Serializer

Expand Down Expand Up @@ -92,6 +95,27 @@ def serialize(self, serializer: Serializer):
serializer.to_bytes(self.key.encode())


class MultiEd25519PublicKey:
LENGTH: int = 32

key: VerifyKey
keys: List[PublicKey]
threshold: int

def __init__(self, keys: List[PublicKey], threshold: int):
self.keys = keys
self.threshold = threshold
hasher = hashlib.sha3_256()
concatenated_keys = bytes()
for key in keys:
concatenated_keys += key.key.encode()
hasher.update(concatenated_keys + bytes([threshold]) + b"\x01")
self.key = VerifyKey(hasher.digest())

def __str__(self) -> str:
return f"0x{self.key.encode().hex()}"


class Signature:
LENGTH: int = 64

Expand Down Expand Up @@ -123,6 +147,14 @@ def serialize(self, serializer: Serializer):
serializer.to_bytes(self.signature)


class MultiEd25519Signature:

signatures: List[Tuple[PublicKey, Signature]]

def __init__(self, signatures):
self.signatures = signatures


class Test(unittest.TestCase):
def test_sign_and_verify(self):
in_value = b"test_message"
Expand Down
104 changes: 104 additions & 0 deletions ecosystem/python/sdk/examples/multisig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import time

from aptos_sdk.account import Account
from aptos_sdk.account_address import AccountAddress
from aptos_sdk.authenticator import MultiEd25519Authenticator
from aptos_sdk.bcs import Serializer
from aptos_sdk.client import FaucetClient, RestClient
from aptos_sdk.ed25519 import MultiEd25519PublicKey, MultiEd25519Signature
from aptos_sdk.transactions import (
EntryFunction,
RawTransaction,
TransactionArgument,
TransactionPayload
)
from aptos_sdk.type_tag import TypeTag, StructTag

from common import NODE_URL, FAUCET_URL

if __name__ == '__main__':

# :!:>section_1
alice = Account.generate()
bob = Account.generate()
chad = Account.generate()

print("\n=== Account addresses ===")
print(f"Alice: {alice.address()}")
print(f"Bob: {bob.address()}")
print(f"Chad: {chad.address()}")

print("\n=== Authentication keys ===")
print(f"Alice: {alice.auth_key()}")
print(f"Bob: {bob.auth_key()}")
print(f"Chad: {chad.auth_key()}")

print("\n=== Public keys ===")
print(f"Alice: {alice.public_key()}")
print(f"Bob: {bob.public_key()}")
print(f"Chad: {chad.public_key()}") # <:!:section_1

input("\nPress Enter to continue...")

# :!:>section_2
threshold = 2
multisig_public_key = MultiEd25519PublicKey(
[alice.public_key(), bob.public_key(), chad.public_key()], threshold)
multisig_address = AccountAddress.from_multisig_schema(
[alice.public_key(), bob.public_key(), chad.public_key()], threshold)

print("\n=== 2-of-3 Multisig account ===")
print(f"Account public key: {multisig_public_key}")
print(f"Account address: {multisig_address}") # <:!:section_2

input("\nPress Enter to continue...")

# :!:>section_3
rest_client = RestClient(NODE_URL)
faucet_client = FaucetClient(FAUCET_URL, rest_client)

print("\n=== Funding accounts ===")
faucet_client.fund_account(alice.address() , 10_000_000)
print(f"Alice's balance: {rest_client.account_balance(alice.address())}")
faucet_client.fund_account(bob.address() , 20_000_000)
print(f"Bob's balance: {rest_client.account_balance(bob.address())}")
faucet_client.fund_account(chad.address() , 30_000_000)
print(f"Chad's balance: {rest_client.account_balance(chad.address())}")
faucet_client.fund_account(multisig_address, 40_000_000)
print(f"Multisig balance: {rest_client.account_balance(multisig_address)}") # <:!:section_3

input("\nPress Enter to continue...")

# :!:>section_4
entry_function = EntryFunction.natural(
module="0x1::coin",
function="transfer",
ty_args=[TypeTag(StructTag.from_str("0x1::aptos_coin::AptosCoin"))],
args=[TransactionArgument(chad.address(), Serializer.struct),
TransactionArgument(100, Serializer.u64)])

raw_transaction = RawTransaction(
sender=multisig_address,
sequence_number=0,
payload=TransactionPayload(entry_function),
max_gas_amount=rest_client.client_config.max_gas_amount,
gas_unit_price=rest_client.client_config.gas_unit_price,
expiration_timestamps_secs=(
int(time.time()) + rest_client.client_config.expiration_ttl),
chain_id=rest_client.chain_id)

alice_signature = raw_transaction.sign(alice.private_key)
bob_signature = raw_transaction.sign(bob.private_key)

print("\n=== Individual signatures ===")
print(f"Alice: {alice_signature}")
print(f"Bob: {bob_signature}") # <:!:section_4

input("\nPress Enter to continue...")

# :!:>section_5
multisig_signature = MultiEd25519Signature(
[(alice.public_key(), alice_signature),
(bob.public_key() , bob_signature)])
authenticator = MultiEd25519Authenticator(
multisig_public_key, multisig_signature) # <:!:section_5