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 5 commits
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
151 changes: 151 additions & 0 deletions developer-docs-site/docs/tutorials/first-multisig.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
---
title: "Your First Multisig"
slug: "your-first-multisig"
---

# Your First Multisig

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

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

:::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.md).

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

## 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, we 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 output should resemble:

```zsh
=== Account addresses ===
Alice: 0x0d13819690aaf14c00538bd50879d96d4763690d112390b7d7994766201e75fe
Bob: 0xe8221d30a0f50586d193c5293dd3d89f768a6f13e089aec3c55c0d4a9c748f4d
Chad: 0xdb3ed5b3e53c9793eaba242cbc8e4e776c0eb8f7e10341784de7961fa5599dac

=== Authentication keys ===
Alice: 0x0d13819690aaf14c00538bd50879d96d4763690d112390b7d7994766201e75fe
Bob: 0xe8221d30a0f50586d193c5293dd3d89f768a6f13e089aec3c55c0d4a9c748f4d
Chad: 0xdb3ed5b3e53c9793eaba242cbc8e4e776c0eb8f7e10341784de7961fa5599dac

=== Public keys ===
Alice: 0xdd14d7b52d8e120ccf8cbad2e51ed49b87554adf96f5df216d77a9103ee01cf4
Bob: 0x798d0eeaddbe0d078088034b35d36e77c522e76a072aa3ff127b2e3e13b50446
Chad: 0xd386988ee35e709a721c848976d143cdc83bf67295f4089558135364aeab22b7
```

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

## Step 4: Generate a multisig account

Next generate a [K-of-N multisigner](../concepts/accounts.md#multisigner-authentication) public key and account address for a multisig account requiring two of the three 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 the output should resemble:

```zsh
=== 2-of-3 Multisig account ===
Account public key: 2-of-3 Multi-Ed25519 public key
Account address: 0x61c6fc315d6a96bd84b93c3ee89466a7d9323c425499687b1c4e275942443bac
```

## 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 two-of-three multisig account, signatures are required only from two individual signers.

### Step 6.1: Gather individual signatures

First generate a raw transaction, signed by Alice and 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: 0x7960fb9dac861fb46b43c31275df88f24e309d0c94d0a07c7898644044d200213286ed64aef37172e9ae58cbc1dc224e925c657c8f796899d1edc0f41f3fe30f
Bob: 0x6197b02eaa8e5d378e8edc7993065e88d8d3ce52a1193bc1bb5ad3ed91e37157a02fd37cb2ef3475d3b05083e3b3b9731f9da43afee110047d95bd9296896d08
```

### Step 6.2: Submit the multisig transaction

Next generate a [multisig authenticator](../guides/sign-a-transaction.md#multisignature-transactions) and submit the transaction:


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

```zsh
=== Submitting transaction ===
Transaction hash: 0x3a65087a4e5e6ab3b3eb56d222509090d3cdafbdb907deb2c233069d308d8a81
```

### Step 6.3: Check balances

Check the new account balances:

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

```zsh
=== New account balances===
Alice's balance: 10000000
Bob's balance: 20000000
Chad's balance: 30000100
Multisig balance: 39945700
```

Note that even though Alice and Bob signed the transaction, their account balances have not changed.
Chad, however, has received 100 octas from the multisig account, which assumed the gas costs of the transaction and thus has had more than 100 octas deducted.
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](first-multisig.md)

Learn how to perform assorted operations using [K-of-N multisigner authentication](../concepts/accounts.md#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.auth_key())

@staticmethod
def deserialize(deserializer: Deserializer) -> AccountAddress:
return AccountAddress(deserializer.fixed_bytes(AccountAddress.LENGTH))
Expand Down
18 changes: 11 additions & 7 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,15 +146,19 @@ def serialize(self, serializer: Serializer):


class MultiEd25519Authenticator:
def __init__(self):
raise NotImplementedError
public_key: ed25519.MultiEd25519PublicKey
signature: ed25519.MultiEd25519Signature

def __init__(self, public_key, signature):
self.public_key = public_key
self.signature = signature

def verify(self, data: bytes) -> bool:
raise NotImplementedError

@staticmethod
def deserialize(deserializer: Deserializer) -> MultiEd25519Authenticator:
raise NotImplementedError
def to_bytes(self):
self.public_key.to_bytes() + self.signature.to_bytes()

def serialize(self, serializer: Serializer):
raise NotImplementedError
serializer.struct(self.public_key)
serializer.struct(self.signature)
56 changes: 56 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,32 @@ def serialize(self, serializer: Serializer):
serializer.to_bytes(self.key.encode())


class MultiEd25519PublicKey:
keys: List[PublicKey]
threshold: int

def __init__(self, keys: List[PublicKey], threshold: int):
self.keys = keys
self.threshold = threshold

def __str__(self) -> str:
return f"{self.threshold}-of-{len(self.keys)} Multi-Ed25519 public key"

def auth_key(self) -> bytes:
hasher = hashlib.sha3_256()
hasher.update(self.to_bytes() + b"\x01")
return hasher.digest()

def to_bytes(self) -> bytes:
concatenated_keys = bytes()
for key in self.keys:
concatenated_keys += key.key.encode()
return concatenated_keys + bytes([self.threshold])

def serialize(self, serializer: Serializer):
serializer.to_bytes(self.to_bytes())


class Signature:
LENGTH: int = 64

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


class MultiEd25519Signature:
signatures: List[Signature]
bitmap: bytes

def __init__(self,
public_key: MultiEd25519PublicKey,
signatures_map: List[Tuple[PublicKey, Signature]]):
self.signatures = list()
bitmap = 0
for entry in signatures_map:
self.signatures.append(entry[1])
index = public_key.keys.index(entry[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 to_bytes(self) -> bytes:
concatenated_signatures = bytes()
for signature in self.signatures:
concatenated_signatures += signature.data()
return concatenated_signatures + self.bitmap

def serialize(self, serializer: Serializer):
serializer.to_bytes(self.to_bytes())


class Test(unittest.TestCase):
def test_sign_and_verify(self):
in_value = b"test_message"
Expand Down
Loading