Skip to content

Commit

Permalink
References #161
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankC01 committed Sep 1, 2023
1 parent e770d2d commit 8e75b9b
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 73 deletions.
154 changes: 91 additions & 63 deletions pysui/sui/sui_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,8 @@ def __repr__(self) -> str:
return f"PubKey {self._public_key}, PrivKey `Private`"


class MultiSig:
"""Multi signature support."""
class BaseMultiSig:
"""."""

_MIN_KEYS: int = 2
_MAX_KEYS: int = 10
Expand All @@ -231,35 +231,24 @@ class MultiSig:
_COMPRESSED_SIG_LEN: int = 65

def __init__(
self, suikeys: list[SuiKeyPair], weights: list[int], threshold: int
self,
sui_pub_keys: list[SuiPublicKey],
weights: list[int],
threshold: int,
):
"""__init__ Initiate a MultiSig object.
Note that Sui multi-sig accepts up to a maximum of ten (10) individual signer keys.
:param suikeys: The list of keys participating in the multi-sig signing operations.
:type suikeys: list[SuiKeyPair]
:param weights: Corresponding weights for each key. Max value of each weight is 255 (8 bit unsigned)
:type weights: list[int]
:param threshold: The threshold criteria for this MultiSig. Max value is 2549 (16 bit unsigned)
:type threshold: int
"""
"""Validating initialization of components."""
if (
len(suikeys) in range(self._MIN_KEYS, self._MAX_KEYS)
and len(suikeys) == len(weights)
len(sui_pub_keys) in range(self._MIN_KEYS, self._MAX_KEYS)
and len(sui_pub_keys) == len(weights)
and threshold <= self._MAX_THRESHOLD
and max(weights) <= self._MAX_WEIGHT
and sum(weights) >= threshold
):
self._keys: list[SuiKeyPair] = suikeys
self._scheme: SignatureScheme = SignatureScheme.MULTISIG
self._weights: list[int] = weights
self._public_keys = sui_pub_keys
self._threshold: int = threshold
self._scheme: SignatureScheme = SignatureScheme.MULTISIG
self._address: SuiAddress = self._multi_sig_address()

self._public_keys: list[SuiPublicKey] = [
x.public_key for x in self._keys # type: ignore
]
else:
raise ValueError("Invalid arguments provided to constructor")

Expand All @@ -272,8 +261,8 @@ def _multi_sig_address(self) -> SuiAddress:
# Build the digest to generate a SuiAddress (hash) from
digest = self._scheme.to_bytes(1, "little")
digest += self._threshold.to_bytes(2, "little")
for index, kkeys in enumerate(self._keys):
digest += kkeys.public_key.scheme_and_key() # type: ignore
for index, kkeys in enumerate(self._public_keys):
digest += kkeys.scheme_and_key() # type: ignore
digest += self._weights[index].to_bytes(1, "little")
return SuiAddress(hashlib.blake2b(digest, digest_size=32).hexdigest())

Expand Down Expand Up @@ -305,11 +294,6 @@ def public_keys(self) -> list[SuiPublicKey]:
"""Return a copy of the list of SuiPublicKeys used in this MultiSig."""
return self._public_keys.copy()

@property
def full_keys(self) -> list[SuiKeyPair]:
"""."""
return self._keys.copy()

@property
def weights(self) -> list[int]:
"""Return a copy of the list of weights used in this MultiSig."""
Expand Down Expand Up @@ -339,25 +323,12 @@ def validate_signers(
return hit_indexes
raise ValueError("Keys and weights for signing do not meet thresholds")

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"
hit_indexes = [self._public_keys.index(i) for i in pub_keys]
# If all inbound pubkeys have reference to item in ms list
assert len(hit_indexes) == len(
pub_keys
), "Public key not part of MultiSig keys"
return hit_indexes

def _new_publickey(self) -> list[MsNewPublicKey]:
"""Generate MultiSig BCS representation of PublicKey."""
# Generate new BCS PublicKeys from the FULL compliment of original public keys
pks: list[MsNewPublicKey] = []
for index, kkeys in enumerate(self._keys):
pkb = kkeys.public_key.key_bytes # type: ignore
for index, kkeys in enumerate(self._public_keys):
pkb = kkeys.key_bytes # type: ignore
if kkeys.scheme == SignatureScheme.ED25519:
npk = MsNewPublicKey(
"Ed25519",
Expand All @@ -376,6 +347,79 @@ def _new_publickey(self) -> list[MsNewPublicKey]:
pks.append(npk)
return pks

def _signature(
self,
pub_keys: list[SuiPublicKey],
compressed_sigs: list[MsCompressedSig],
) -> SuiSignature:
"""."""
key_indices = self.validate_signers(pub_keys)
# Generate the public keys used position bitmap
# then build the signature
bm_pks: int = 0
for index in key_indices:
bm_pks |= 1 << index
serialized_rbm: MsBitmap = MsBitmap(bm_pks)

msig_signature = MultiSignature(
self._scheme,
compressed_sigs,
serialized_rbm,
self._new_publickey(),
self.threshold,
)
return SuiSignature(
base64.b64encode(msig_signature.serialize()).decode()
)

def signature_from(
self, pub_keys: list[SuiPublicKey], signatures: list[SuiSignature]
) -> SuiSignature:
"""signature_from Creates a multisig signature from signed bytes.
:param pub_keys: List of public keys associated to keypairs that created signatures
:type pub_keys: list[SuiPublicKey]
:param signatures: Signatures from signed transaction bytes digest
:type signatures: list[SuiSignature]
:return: A multisig signature
:rtype: SuiSignature
"""
compressed: list[MsCompressedSig] = []
for index in signatures:
sig = str(index.value)
compressed.append(
MsCompressedSig(
list(base64.b64decode(sig)[0 : self._COMPRESSED_SIG_LEN])
)
)
return self._signature(pub_keys, compressed)


class MultiSig(BaseMultiSig):
"""Multi signature support."""

def __init__(
self, suikeys: list[SuiKeyPair], weights: list[int], threshold: int
):
"""__init__ Initiate a MultiSig object.
Note that Sui multi-sig accepts up to a maximum of ten (10) individual signer keys.
:param suikeys: The list of keys participating in the multi-sig signing operations.
:type suikeys: list[SuiKeyPair]
:param weights: Corresponding weights for each key. Max value of each weight is 255 (8 bit unsigned)
:type weights: list[int]
:param threshold: The threshold criteria for this MultiSig. Max value is 2549 (16 bit unsigned)
:type threshold: int
"""
super().__init__([kp.public_key for kp in suikeys], weights, threshold)
self._keys = suikeys

@property
def full_keys(self) -> list[SuiKeyPair]:
"""."""
return self._keys.copy()

@versionadded(
version="0.21.1", reason="Support for inline multisig signing"
)
Expand Down Expand Up @@ -406,29 +450,13 @@ def sign(
) -> SuiSignature:
"""sign Signs transaction bytes for operation that changes objects owned by MultiSig address."""
# Validate the pub_keys alignment with self._keys
key_indices = self._validate_signers(pub_keys)
# key_indices = 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 # type: ignore
compressed_sigs: list[MsCompressedSig] = self._compressed_signatures(
str(tx_bytes), key_indices
)

# Generate the public keys used position bitmap
# then build the signature
bm_pks: int = 0
for index in key_indices:
bm_pks |= 1 << index
serialized_rbm: MsBitmap = MsBitmap(bm_pks)
msig_signature = MultiSignature(
self._scheme,
compressed_sigs,
serialized_rbm,
self._new_publickey(),
self.threshold,
)
return SuiSignature(
base64.b64encode(msig_signature.serialize()).decode()
str(tx_bytes), self.validate_signers(pub_keys)
)
return self._signature(pub_keys, compressed_sigs)

def serialize(self) -> str:
"""serialize Serializes the MultiSig object to base64 string.
Expand Down
36 changes: 36 additions & 0 deletions pysui/sui/sui_txn/async_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,42 @@ async def execute(
self._executed = True
return await self.client.execute(exec_tx)

@versionadded(version="0.35.0", reason="Added for offline signing support")
async def deferred_execution(
self,
*,
gas_budget: Optional[Union[str, SuiString]] = "",
use_gas_object: Optional[Union[str, ObjectID]] = None,
run_verification: Optional[bool] = False,
) -> Union[str, ValueError]:
"""deferred_execution Finalizes transaction and returns base64 string for signing.
The result can then be signed (single, multisig, etc) and then executed
:param gas_budget: The gas budget to use. If set, it will be used in
transaction. Otherwise a dry-run is peformed to set budget, default to empty string (None)
:type gas_budget: Optional[Union[str, SuiString]], optional
:param use_gas_object: Explicit gas object to use for payment, defaults to None.
Will fail if provided object is marked as 'in use' in commands.
:type use_gas_object: Optional[Union[str, ObjectID]], optional
:param run_verification: Will run validation on transaction using Sui ProtocolConfig constraints, defaults to False.
Will fail if validation errors (SuiRpcResult.is_err()).
:type run_verification: Optional[bool], optional
:return: The bsae64 encoded transaction bytes that can be signed for execution
:rtype: str
"""
assert not self._executed, "Transaction already executed"
txn_data = await self._build_for_execute(gas_budget, use_gas_object)
ser_data = txn_data.serialize()
if run_verification:
_, failed_verification = self.verify_transaction(ser_data)
if failed_verification:
return SuiRpcResult(
False, "Failed validation", failed_verification
)
self._executed = True
return base64.b64encode(ser_data).decode()

@versionchanged(
version="0.16.1",
reason="Added returning SuiRpcResult if inspect transaction failed.",
Expand Down
15 changes: 10 additions & 5 deletions pysui/sui/sui_txn/signing_ms.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pysui import SuiAddress, SyncClient, handle_result, ObjectID, AsyncClient
from pysui.sui.sui_builders.base_builder import sui_builder
from pysui.sui.sui_builders.exec_builders import _MoveCallTransactionBuilder
from pysui.sui.sui_crypto import MultiSig, SuiPublicKey
from pysui.sui.sui_crypto import MultiSig, SuiPublicKey, BaseMultiSig
from pysui.sui.sui_txresults.single_tx import SuiCoinObject
from pysui.sui.sui_types.collections import SuiArray
from pysui.sui.sui_types.scalars import SuiSignature, SuiString
Expand Down Expand Up @@ -61,15 +61,17 @@ def __init__(
@versionadded(
version="0.21.1", reason="Support subset pubkey address generation"
)
@versionchanged(version="0.35.0", reason="Change msig to BaseMultiSig")
class SigningMultiSig:
"""Wraps the mutli-sig along with pubkeys to use in SuiTransaction."""

def __init__(self, msig: MultiSig, pub_keys: list[SuiPublicKey]):
def __init__(self, msig: BaseMultiSig, 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
self._can_sign_msg = isinstance(msig, MultiSig)

@property
def signing_address(self) -> str:
Expand Down Expand Up @@ -174,9 +176,12 @@ def get_signatures(
)
)
else:
sig_list.append(
signer.multi_sig.sign(tx_bytes, signer.pub_keys)
)
if signer._can_sign_msg:
sig_list.append(
signer.multi_sig.sign(tx_bytes, signer.pub_keys)
)
else:
raise ValueError("BaseMultiSig can not sign in execution")
return SuiArray(sig_list)


Expand Down
42 changes: 37 additions & 5 deletions pysui/sui/sui_txn/sync_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,11 +365,6 @@ def execute(
)

tx_b64 = base64.b64encode(ser_data).decode()
# result = self.client.execute(
# DryRunTransaction(tx_bytes=base64.b64encode(tx_b64).decode())
# )
# if result.is_ok():
# pass

exec_tx = ExecuteTransaction(
tx_bytes=tx_b64,
Expand All @@ -382,6 +377,43 @@ def execute(
self._executed = True
return self.client.execute(exec_tx)

@versionadded(version="0.35.0", reason="Added for offline signing support")
def deferred_execution(
self,
*,
gas_budget: Optional[Union[str, SuiString]] = "",
use_gas_object: Optional[Union[str, ObjectID]] = None,
run_verification: Optional[bool] = False,
) -> Union[str, ValueError]:
"""deferred_execution Finalizes transaction and returns base64 string for signing.
The result can then be signed (single, multisig) and then executed
:param gas_budget: The gas budget to use. If set, it will be used in
transaction. Otherwise a dry-run is peformed to set budget, default to empty string (None)
:type gas_budget: Optional[Union[str, SuiString]], optional
:param use_gas_object: Explicit gas object to use for payment, defaults to None.
Will fail if provided object is marked as 'in use' in commands.
:type use_gas_object: Optional[Union[str, ObjectID]], optional
:param run_verification: Will run validation on transaction using Sui ProtocolConfig constraints, defaults to False.
Will fail if validation errors (SuiRpcResult.is_err()).
:type run_verification: Optional[bool], optional
:return: The bsae64 encoded transaction bytes that can be signed for execution
:rtype: str
"""
assert not self._executed, "Transaction already executed"
txn_data = self._build_for_execute(gas_budget, use_gas_object)
ser_data = txn_data.serialize()
if run_verification:
_, failed_verification = self.verify_transaction(ser_data)
if failed_verification:
return SuiRpcResult(
False, "Failed validation", failed_verification
)

self._executed = True
return base64.b64encode(ser_data).decode()

# Argument resolution to lower level types
@versionadded(
version="0.18.0", reason="Reuse for argument nested list recursion."
Expand Down

0 comments on commit 8e75b9b

Please sign in to comment.