Skip to content

Commit

Permalink
[multikey] Add multikey support
Browse files Browse the repository at this point in the history
Add multikey support, and deprecate JSON payloads.  They're obsolete
with BCS payloads.
  • Loading branch information
gregnazario committed Aug 18, 2024
1 parent 88dc5ce commit 96cf595
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

All notable changes to the Aptos Python SDK will be captured in this file. This changelog is written by hand for now.

## Unreleased
- Add Multikey support for Python, with an example
- Deprecate non-BCS transaction submission

## 0.8.6
- add client for graphql indexer service with light demo in coin transfer
- add mypy to ignore missing types for graphql and ecdsa
Expand Down
2 changes: 2 additions & 0 deletions aptos_sdk/account_address.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,8 @@ def from_key(key: asymmetric_crypto.PublicKey) -> AccountAddress:
hasher.update(AuthKeyScheme.MultiEd25519)
elif isinstance(key, asymmetric_crypto_wrapper.PublicKey):
hasher.update(AuthKeyScheme.SingleKey)
elif isinstance(key, asymmetric_crypto_wrapper.MultiPublicKey):
hasher.update(AuthKeyScheme.MultiKey)
else:
raise Exception("Unsupported asymmetric_crypto.PublicKey key type.")

Expand Down
137 changes: 136 additions & 1 deletion aptos_sdk/asymmetric_crypto_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

from __future__ import annotations

from typing import List, Tuple, cast

from . import asymmetric_crypto, ed25519, secp256k1_ecdsa
from .bcs import Deserializer, Serializer

Expand All @@ -29,7 +31,10 @@ def to_crypto_bytes(self) -> bytes:
return ser.output()

def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool:
return self.public_key.verify(data, signature)
# Convert signature to the original signature
sig = cast(Signature, signature)

return self.public_key.verify(data, sig.signature)

@staticmethod
def deserialize(deserializer: Deserializer) -> PublicKey:
Expand Down Expand Up @@ -85,3 +90,133 @@ def deserialize(deserializer: Deserializer) -> Signature:
def serialize(self, serializer: Serializer):
serializer.uleb128(self.variant)
serializer.struct(self.signature)


class MultiPublicKey(asymmetric_crypto.PublicKey):
keys: List[PublicKey]
threshold: int

MIN_KEYS = 2
MAX_KEYS = 32
MIN_THRESHOLD = 1

def __init__(self, keys: List[asymmetric_crypto.PublicKey], threshold: int):
assert (
self.MIN_KEYS <= len(keys) <= self.MAX_KEYS
), f"Must have between {self.MIN_KEYS} and {self.MAX_KEYS} keys."
assert (
self.MIN_THRESHOLD <= threshold <= len(keys)
), f"Threshold must be between {self.MIN_THRESHOLD} and {len(keys)}."

# Ensure keys are wrapped
self.keys = []
for key in keys:
if isinstance(key, PublicKey):
self.keys.append(key)
else:
self.keys.append(PublicKey(key))

self.threshold = threshold

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

def verify(self, data: bytes, signature: asymmetric_crypto.Signature) -> bool:
try:
total_sig = cast(MultiSignature, signature)
assert self.threshold <= len(
total_sig.signatures
), f"Insufficient signatures, {self.threshold} > {len(total_sig.signatures)}"

for idx, signature in total_sig.signatures:
assert (
len(self.keys) > idx
), f"Signature index exceeds available keys {len(self.keys)} < {idx}"
assert self.keys[idx].verify(
data, signature
), "Unable to verify signature"

except Exception:
return False
return True

@staticmethod
def from_crypto_bytes(indata: bytes) -> MultiPublicKey:
deserializer = Deserializer(indata)
return deserializer.struct(MultiPublicKey)

def to_crypto_bytes(self) -> bytes:
serializer = Serializer()
serializer.struct(self)
return serializer.output()

@staticmethod
def deserialize(deserializer: Deserializer) -> MultiPublicKey:
keys = deserializer.sequence(PublicKey.deserialize)
threshold = deserializer.u8()
return MultiPublicKey(keys, threshold)

def serialize(self, serializer: Serializer):
serializer.sequence(self.keys, Serializer.struct)
serializer.u8(self.threshold)


class MultiSignature(asymmetric_crypto.Signature):
signatures: List[Tuple[int, Signature]]
BITMAP_NUM_OF_BYTES: int = 4

def __init__(self, signatures: List[Tuple[int, asymmetric_crypto.Signature]]):
# Sort first to ensure no issues in order
# signatures.sort(key=lambda x: x[0])
self.signatures = []
for index, signature in signatures:
assert (
index < self.BITMAP_NUM_OF_BYTES * 8
), "bitmap value exceeds maximum value"
if isinstance(signature, Signature):
self.signatures.append((index, signature))
else:
self.signatures.append((index, Signature(signature)))

def __eq__(self, other: object):
if not isinstance(other, MultiSignature):
return NotImplemented
return self.signatures == other.signatures

def __str__(self) -> str:
return f"{self.signatures}"

@staticmethod
def deserialize(deserializer: Deserializer) -> MultiSignature:
signatures = deserializer.sequence(Signature.deserialize)
deserializer.uleb128()
bitmap = deserializer.u32()
num_bits = MultiSignature.BITMAP_NUM_OF_BYTES * 8
sig_index = 0
indexed_signatures = []

for i in range(0, num_bits):
has_signature = (bitmap & index_to_bitmap_value(i)) != 0
if has_signature:
indexed_signatures.append((i, signatures[sig_index]))
sig_index += 1

return MultiSignature(signatures)

def serialize(self, serializer: Serializer):
actual_sigs = []
bitmap = 0

for i, signature in self.signatures:
bitmap |= index_to_bitmap_value(i)
actual_sigs.append(signature)

serializer.sequence(actual_sigs, Serializer.struct)
serializer.uleb128(self.BITMAP_NUM_OF_BYTES)
serializer.u32(bitmap)


def index_to_bitmap_value(i: int) -> int:
bit = i % 8
byte = i.__floordiv__(8)
return (128 >> bit) << (byte * 8)
19 changes: 15 additions & 4 deletions aptos_sdk/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import httpx
import python_graphql_client
from typing_extensions import deprecated

from .account import Account
from .account_address import AccountAddress
Expand Down Expand Up @@ -518,8 +519,10 @@ async def submit_and_wait_for_bcs_transaction(
await self.wait_for_transaction(txn_hash)
return await self.transaction_by_hash(txn_hash)

@deprecated("please use bcs_submit_transaction for better performance and security")
async def submit_transaction(self, sender: Account, payload: Dict[str, Any]) -> str:
"""
Deprecated, please use bcs_submit_transaction for better performance and security
1) Generates a transaction request
2) submits that to produce a raw transaction
3) signs the raw transaction
Expand Down Expand Up @@ -716,17 +719,22 @@ async def create_multi_agent_bcs_transaction(

async def create_bcs_transaction(
self,
sender: Account,
sender: Account | AccountAddress,
payload: TransactionPayload,
sequence_number: Optional[int] = None,
) -> RawTransaction:
if isinstance(sender, Account):
sender_address = sender.address()
else:
sender_address = sender

sequence_number = (
sequence_number
if sequence_number is not None
else await self.account_sequence_number(sender.address())
else await self.account_sequence_number(sender_address)
)
return RawTransaction(
sender.address(),
sender_address,
sequence_number,
payload,
self.client_config.max_gas_amount,
Expand All @@ -751,10 +759,13 @@ async def create_bcs_signed_transaction(
# Transaction wrappers
#

@deprecated("please use bcs_transfer for better performance and security")
async def transfer(
self, sender: Account, recipient: AccountAddress, amount: int
) -> str:
"""Transfer a given coin amount from a given Account to the recipient's account address.
"""
Deprecated: please use bcs_transfer for greater performance and security
Transfer a given coin amount from a given Account to the recipient's account address.
Returns the sequence number of the transaction used to transfer."""

payload = {
Expand Down
28 changes: 28 additions & 0 deletions aptos_sdk/authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ def __init__(self, authenticator: typing.Any):
self.variant = AccountAuthenticator.MULTI_ED25519
elif isinstance(authenticator, SingleKeyAuthenticator):
self.variant = AccountAuthenticator.SINGLE_KEY
elif isinstance(authenticator, MultiKeyAuthenticator):
self.variant = AccountAuthenticator.MULTI_KEY
else:
raise Exception("Invalid type")
self.authenticator = authenticator
Expand Down Expand Up @@ -360,3 +362,29 @@ def deserialize(deserializer: Deserializer) -> SingleKeyAuthenticator:
def serialize(self, serializer: Serializer):
serializer.struct(self.public_key)
serializer.struct(self.signature)


class MultiKeyAuthenticator:
public_key: asymmetric_crypto_wrapper.MultiPublicKey
signature: asymmetric_crypto_wrapper.MultiSignature

def __init__(
self,
public_key: asymmetric_crypto_wrapper.MultiPublicKey,
signature: asymmetric_crypto_wrapper.MultiSignature,
):
self.public_key = public_key
self.signature = signature

def verify(self, data: bytes) -> bool:
return self.public_key.verify(data, self.signature)

@staticmethod
def deserialize(deserializer: Deserializer) -> MultiKeyAuthenticator:
public_key = deserializer.struct(asymmetric_crypto_wrapper.MultiPublicKey)
signature = deserializer.struct(asymmetric_crypto_wrapper.MultiSignature)
return MultiKeyAuthenticator(public_key, signature)

def serialize(self, serializer: Serializer):
serializer.struct(self.public_key)
serializer.struct(self.signature)
4 changes: 2 additions & 2 deletions examples/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
# :!:>section_1
FAUCET_URL = os.getenv(
"APTOS_FAUCET_URL",
"https://faucet.devnet.aptoslabs.com",
"http://localhost:8081",
)
INDEXER_URL = os.getenv(
"APTOS_INDEXER_URL",
"https://api.devnet.aptoslabs.com/v1/graphql",
)
NODE_URL = os.getenv("APTOS_NODE_URL", "https://api.devnet.aptoslabs.com/v1")
NODE_URL = os.getenv("APTOS_NODE_URL", "http://localhost:8080/v1")
# <:!:section_1
Loading

0 comments on commit 96cf595

Please sign in to comment.