Skip to content

Commit

Permalink
Closes #100, #109
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankC01 committed May 9, 2023
1 parent e960816 commit 6f7d4dd
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 21 deletions.
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.21.1] - Unpublished

### Added

- Pure python multisig signing, reducing dependencies on Sui binaries [enhancement](https://github.com/FrankC01/pysui/issues/100)

### Fixed

- Use of multisigs in SuiTransaction [bug](https://github.com/FrankC01/pysui/issues/109)

### Changed

### Removed

## [0.21.0] - 2023-05-09

### Added
Expand Down
2 changes: 1 addition & 1 deletion doc/source/multi_sig.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Signing Transactions using a MultiSig
You need to sign transactions that change any object owned by the MultiSig address. This is where the
heightened security and governance aspect come into play.

* Note that ``pysui``, at this time, does not perform the actual signing but rather invokes the ``sui keytool multi-sig-combine-partial-sig ...`` command line. We are working on removing this dependency.
With version '0.21.1' of ``pysui`` signing with MultiSig no longer relies on Sui binaries keytool

In the following scenarios it is assumed that the admin does not participate in the signing. This example uses
the first two keys that are in the MultiSig but in a real world scenario it is likely the
Expand Down
28 changes: 23 additions & 5 deletions pysui/sui/sui_clients/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import base64
import os
import hashlib
from pathlib import Path
from typing import Any, Optional, Union, Callable
from deprecated.sphinx import versionadded, versionchanged
Expand Down Expand Up @@ -53,15 +54,23 @@


@versionadded(version="0.17.0", reason="Standardize on signing permutations")
@versionadded(version="0.21.1", reason="Support subset pubkey address generation")
class SigningMultiSig:
"""Wraps the mutli-sig along with pubkeys to use in SuiTransaction."""

def __init__(self, msig: MultiSig, pub_keys: list[SuiPublicKey]):
"""."""
self.multi_sig = msig
self.pub_keys = pub_keys
self.indicies = msig.validate_signers(pub_keys)
self._address = self.multi_sig.address


@property
def signing_address(self) -> str:
"""."""
return self._address

@versionadded(version="0.17.0", reason="Standardize on signing permutations")
@versionchanged(version="0.18.0", reason="Removed additional_signers as not supported by Sui at the moment.")
class SignerBlock:
Expand Down Expand Up @@ -149,6 +158,7 @@ def get_signatures(self, *, client: SuiClient, tx_bytes: str) -> SuiArray[SuiSig
# sig_list.append(signer.multi_sig.sign(tx_bytes, signer.pub_keys))
return SuiArray(sig_list)

@versionchanged(version="0.21.1", reason="Corrected when using multisig senders.")
def get_gas_object(
self, *, client: SuiClient, budget: int, objects_in_use: list, merge_coin: bool, gas_price: int
) -> bcs.GasData:
Expand All @@ -158,8 +168,14 @@ def get_gas_object(
# If both not set, Fail
if not who_pays:
raise ValueError("Both SuiTransaction sponor and sender are null. Complete those before execute.")
who_pays = who_pays if isinstance(who_pays, SuiAddress) else who_pays.multi_sig.as_sui_address
owner_coins: list[SuiCoinObject] = handle_result(client.get_gas(who_pays)).data
if isinstance(who_pays,SuiAddress):
whose_gas = who_pays
who_pays = who_pays.address
else:
whose_gas = who_pays.signing_address # as_sui_address
who_pays = who_pays.signing_address
# who_pays = who_pays if isinstance(who_pays, SuiAddress) else who_pays.multi_sig.as_sui_address
owner_coins: list[SuiCoinObject] = handle_result(client.get_gas(whose_gas)).data
# Get whatever gas objects below to whoever is paying
# and filter those not in use
use_coin: SuiCoinObject = None
Expand Down Expand Up @@ -204,7 +220,7 @@ def get_gas_object(
bcs.Digest.from_str(use_coin.digest),
)
],
bcs.Address.from_str(who_pays.address),
bcs.Address.from_str(whose_gas),
gas_price,
budget,
)
Expand Down Expand Up @@ -244,7 +260,8 @@ class SuiTransaction:

_TRANSACTION_GAS_ARGUMENT: bcs.Argument = bcs.Argument("GasCoin")

def __init__(self, client: SuiClient, merge_gas_budget: bool = False, initial_sender: SuiAddress = False) -> None:
@versionchanged(version="0.21.1", reason="Takes a 'initial_sender' as option.")
def __init__(self, client: SuiClient, merge_gas_budget: bool = False, initial_sender: Union[SuiAddress,SigningMultiSig] = False) -> None:
"""Transaction initializer."""
self.builder = tx_builder.ProgrammableTransactionBuilder()
self.client = client
Expand Down Expand Up @@ -346,6 +363,7 @@ def inspect_for_cost(self) -> Union[tuple[int, int, str], SuiRpcResult]:

@versionchanged(version="0.17.0", reason="Only used internally.")
@versionchanged(version="0.17.0", reason="Reworked using SignerBlock gas resolution.")
@versionchanged(version="0.21.1", reason="Corrected using multisig senders.")
def _build_for_execute(
self,
gas_budget: Union[str, SuiString],
Expand Down Expand Up @@ -379,7 +397,7 @@ def _build_for_execute(
if isinstance(self.signer_block.sender, SuiAddress):
who_sends = self.signer_block.sender.address
else:
who_sends = self.signer_block.sender.multi_sig.address
who_sends = self.signer_block.sender.signing_address
return bcs.TransactionData(
"V1",
bcs.TransactionDataV1(
Expand Down
33 changes: 19 additions & 14 deletions pysui/sui/sui_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import subprocess
import json
from typing import Union
from deprecated.sphinx import versionadded, versionchanged
from deprecated.sphinx import versionadded, versionchanged,deprecated
import pyroaring
import secp256k1
import bip_utils
Expand Down Expand Up @@ -432,7 +432,8 @@ def threshold(self) -> int:
"""Return the threshold amount used in this MultiSig."""
return self._threshold

def _validate_signers(self, pub_keys: list[SuiPublicKey]) -> list[int]:
@versionchanged(version="0.21.1", reason="Exposed as public for use by SuiTransaction")
def validate_signers(self, pub_keys: list[SuiPublicKey]) -> Union[list[int],ValueError]:
"""Validate pubkeys part of multisig and have enough weight."""
# Must be subset of full ms list
if len(pub_keys) <= len(self._public_keys):
Expand All @@ -441,9 +442,10 @@ def _validate_signers(self, pub_keys: list[SuiPublicKey]) -> list[int]:
if len(hit_indexes) == len(pub_keys):
if sum([self._weights[x] for x in hit_indexes]) >= self._threshold:
return hit_indexes
return None
raise ValueError("Keys and weights for signing do not meet thresholds")

def sign(self, tx_bytes: Union[str, SuiTxBytes], pub_keys: list[SuiPublicKey]) -> Union[int, SuiSignature]:
@deprecated(version="0.21.1",reason="New version performs signing without invoking binaries")
def _old_sign(self, tx_bytes: Union[str, SuiTxBytes], pub_keys: list[SuiPublicKey]) -> Union[int, SuiSignature]:
"""sign Signs transaction bytes for operation that changes objects owned by MultiSig address.
:param tx_bytes: Transaction bytes base64 string from 'unsafe...' result or Transaction BCS
Expand All @@ -455,7 +457,7 @@ def sign(self, tx_bytes: Union[str, SuiTxBytes], pub_keys: list[SuiPublicKey]) -
:rtype: SuiSignature
"""
# Validate the pub_keys align to self._keys
key_indx = self._validate_signers(pub_keys)
key_indx = self.validate_signers(pub_keys)
if key_indx:
tx_bytes = tx_bytes if isinstance(tx_bytes, str) else tx_bytes.value
# Build the command line for `sui keytool multi-sig-combine-partial-sig`
Expand Down Expand Up @@ -485,7 +487,7 @@ def sign(self, tx_bytes: Union[str, SuiTxBytes], pub_keys: list[SuiPublicKey]) -
return result.returncode
raise ValueError("Invalid signer pub_keys")

def _test_validate_signers(self, pub_keys: list[SuiPublicKey]) -> list[int]:
def _validate_signers(self, pub_keys: list[SuiPublicKey]) -> list[int]:
"""Validate pubkeys part of multisig and have enough weight."""
# Must be subset of full ms list
assert len(pub_keys) <= len(self._public_keys), "More public keys than MultiSig"
Expand All @@ -496,8 +498,9 @@ def _test_validate_signers(self, pub_keys: list[SuiPublicKey]) -> list[int]:
weights = [self._weights[x] for x in hit_indexes]
return hit_indexes, list(zip(pub_keys, weights))

@versionadded(version="0.21.1",reason="Support for inline multisig signing")
def _compressed_signatures(self, tx_bytes: str, key_indices: list[int]) -> list[MsCompressedSig]:
"""."""
"""Creates compressed signatures from each of the signing keys present."""
compressed: list[MsCompressedSig] = []
for index in key_indices:
compressed.append(
Expand All @@ -511,20 +514,22 @@ def _compressed_signatures(self, tx_bytes: str, key_indices: list[int]) -> list[
)
return compressed

def test_sign(self, tx_bytes: Union[str, SuiTxBytes], pub_keys: list[SuiPublicKey]) -> SuiSignature:
@versionadded(version="0.21.1", reason="Full signature creation without binaries.")
def sign(self, tx_bytes: Union[str, SuiTxBytes], pub_keys: list[SuiPublicKey]) -> SuiSignature:
"""sign Signs transaction bytes for operation that changes objects owned by MultiSig address."""
# Validate the pub_keys alignment with self._keys
key_indices, pk_map = self._test_validate_signers(pub_keys)
# Generate BCS compressed signatures
key_indices, pk_map = self._validate_signers(pub_keys)
# Generate BCS compressed signatures for the subset of keys
tx_bytes = tx_bytes if isinstance(tx_bytes, str) else tx_bytes.value
compressed_sigs: list[MsCompressedSig] = self._compressed_signatures(tx_bytes, key_indices)
# Generate BCS Roaring bitmap
serialized_rbm: MsRoaring = MsRoaring(list(pyroaring.BitMap(key_indices).serialize()))
# Generate BCS PublicKeys
# Generate BCS PublicKeys from the FULL compliment of original public keys
pks: list[MsPublicKey] = []
for pk, wght in pk_map:
pk = base64.b64encode(pk.scheme_and_key()).decode().encode(encoding="utf8")
pks.append(MsPublicKey(list(pk), wght))
for index, kkeys in enumerate(self._keys):
pk = base64.b64encode(kkeys.public_key.scheme_and_key()).decode().encode(encoding="utf8")
pks.append(MsPublicKey(list(pk), self._weights[index]))

# Build the BCS MultiSignature
msig_signature = MultiSignature(self._schema, compressed_sigs, serialized_rbm, pks, self.threshold)
return SuiSignature(base64.b64encode(msig_signature.serialize()).decode())
Expand Down
2 changes: 1 addition & 1 deletion pysui/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
# -*- coding: utf-8 -*-

"""Pysui Version."""
__version__ = "0.21.0"
__version__ = "0.21.1"

0 comments on commit 6f7d4dd

Please sign in to comment.