Skip to content

Commit

Permalink
Tweaks to result values based on SUI 0.22.1. Added secp256r1 support. C…
Browse files Browse the repository at this point in the history
…loses #59
  • Loading branch information
FrankC01 committed Jan 24, 2023
1 parent 1775b15 commit 8ae11bb
Show file tree
Hide file tree
Showing 6 changed files with 91 additions and 57 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- More documentation
- Support for `secp256r1` [feature](https://github.com/FrankC01/pysui/issues/59)
- Caveat: Key recovery not supported yet
- Caveat: Due to the requirement of generating a recovery ID a brute force hack was added to signing with secp256r1 keys. However; this requirement for recovery ID is going to be lifted at some point in SUI [feature](https://github.com/MystenLabs/sui/issues/5654)

### Fixed

Expand Down
6 changes: 3 additions & 3 deletions pysui/abstracts/client_keypair.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,11 @@ class PrivateKey(Key):
"""PrivateKey construct."""

@abstractmethod
def sign(self, data: bytes) -> bytes:
def sign(self, data: bytes, recovery_id: int = 0) -> bytes:
"""Sign data and return signature bytes."""

@abstractmethod
def sign_secure(self, public_key: PublicKey, tx_data: bytes) -> bytes:
def sign_secure(self, public_key: PublicKey, tx_data: bytes, recovery_id: int = 0) -> bytes:
"""Sign data securely, returning signature."""


Expand All @@ -105,7 +105,7 @@ def private_key(self) -> PrivateKey:
"""Get the keypair public key."""

@abstractmethod
def new_sign_secure(self, tx_data: str) -> AbstractType:
def new_sign_secure(self, tx_data: str, recovery_id: int = 0) -> AbstractType:
"""Sign transactions securley."""

@classmethod
Expand Down
69 changes: 51 additions & 18 deletions pysui/sui/sui_clients/async_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
from pysui.sui.sui_builders.exec_builders import (
DryRunTransaction,
ExecuteTransaction,
ExecuteSerializedTransaction,
Pay,
PaySui,
PayAllSui,
Expand Down Expand Up @@ -120,28 +119,62 @@ async def sign_and_submit(self, signer: SuiAddress, tx_bytes: SuiTxBytes) -> Uni
:return: Result from execution
:rtype: Union[SuiRpcResult, Exception]
"""
_recovery_id = 0
kpair = self.config.keypair_for_address(signer)
if self.version_at_least(0, 22, 0):

async def _inner_sign(recovery_id: int) -> SuiRpcResult:
builder = ExecuteTransaction(
tx_bytes=tx_bytes,
signature=kpair.new_sign_secure(tx_bytes.tx_bytes),
signature=kpair.new_sign_secure(tx_bytes.tx_bytes, recovery_id),
request_type=self.request_type,
)
elif self.version_at_least(0, 18, 0):
builder = ExecuteSerializedTransaction(
tx_bytes=tx_bytes,
signature=kpair.new_sign_secure(tx_bytes.tx_bytes),
request_type=self.request_type,
)
else:
return SuiRpcResult(False, "Unsupported SUI API version")
result = await self._execute(builder)
if result.is_ok():
if "error" in result.result_data:
return SuiRpcResult(False, result.result_data["error"]["message"], None)
# print(json.dumps(result.result_data["result"], indent=2))
result = SuiRpcResult(True, None, builder.handle_return(result.result_data["result"]))
return result
result = await self._execute(builder)
if result.is_ok() and "error" not in result.result_data:
result = SuiRpcResult(True, None, builder.handle_return(result.result_data["result"]))
elif "error" in result.result_data:
msg = result.result_data["error"]["message"]
if msg == _ClientMixin._SIGNATURE_ERROR and recovery_id > 0:
result = SuiRpcResult(False, msg)
elif msg == _ClientMixin._SIGNATURE_ERROR and recovery_id == 0:
result = await _inner_sign(1)
else:
result = SuiRpcResult(False, msg)
return result

return await _inner_sign(_recovery_id)

# async def sign_and_submit(self, signer: SuiAddress, tx_bytes: SuiTxBytes) -> Union[SuiRpcResult, Exception]:
# """sign_and_submit Signs the transaction bytes from previous submission, signs and executes.

# :param signer: Signer for transaction. Should be the same from original transaction
# :type signer: SuiAddress
# :param tx_bytes: Transaction bytes from previous submission
# :type tx_bytes: SuiTxBytes
# :return: Result from execution
# :rtype: Union[SuiRpcResult, Exception]
# """
# kpair = self.config.keypair_for_address(signer)
# if self.version_at_least(0, 22, 0):
# builder = ExecuteTransaction(
# tx_bytes=tx_bytes,
# signature=kpair.new_sign_secure(tx_bytes.tx_bytes),
# request_type=self.request_type,
# )
# elif self.version_at_least(0, 18, 0):
# builder = ExecuteSerializedTransaction(
# tx_bytes=tx_bytes,
# signature=kpair.new_sign_secure(tx_bytes.tx_bytes),
# request_type=self.request_type,
# )
# else:
# return SuiRpcResult(False, "Unsupported SUI API version")
# result = await self._execute(builder)
# if result.is_ok():
# if "error" in result.result_data:
# return SuiRpcResult(False, result.result_data["error"]["message"], None)
# # print(json.dumps(result.result_data["result"], indent=2))
# result = SuiRpcResult(True, None, builder.handle_return(result.result_data["result"]))
# return result

async def dry_run(self, builder: SuiBaseBuilder) -> Union[SuiRpcResult, Exception]:
"""Submit transaction than sui_dryRunTransaction only."""
Expand Down
1 change: 1 addition & 0 deletions pysui/sui/sui_clients/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class _ClientMixin(Provider):
"""

_RPC_MINIMAL_VERSION: int = 19
_SIGNATURE_ERROR: str = 'Invalid user signature: InvalidSignature { error: "signature error" }.'

def __init__(self, config: SuiConfig, request_type: SuiRequestType = SuiRequestType.WAITFORLOCALEXECUTION) -> None:
"""Client initializer."""
Expand Down
36 changes: 18 additions & 18 deletions pysui/sui/sui_clients/sync_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@
from pysui.sui.sui_builders.exec_builders import (
DryRunTransaction,
ExecuteTransaction,
ExecuteSerializedTransaction,
Pay,
PaySui,
PayAllSui,
Expand Down Expand Up @@ -118,28 +117,29 @@ def sign_and_submit(self, signer: SuiAddress, tx_bytes: SuiTxBytes) -> Union[Sui
:return: Result from execution
:rtype: Union[SuiRpcResult, Exception]
"""
_recovery_id = 0
kpair = self.config.keypair_for_address(signer)
if self.version_at_least(0, 22, 0):

def _inner_sign(recovery_id: int) -> SuiRpcResult:
builder = ExecuteTransaction(
tx_bytes=tx_bytes,
signature=kpair.new_sign_secure(tx_bytes.tx_bytes),
signature=kpair.new_sign_secure(tx_bytes.tx_bytes, recovery_id),
request_type=self.request_type,
)
elif self.version_at_least(0, 18, 0):
builder = ExecuteSerializedTransaction(
tx_bytes=tx_bytes,
signature=kpair.new_sign_secure(tx_bytes.tx_bytes),
request_type=self.request_type,
)
else:
return SuiRpcResult(False, "Unsupported SUI API version")
result = self._execute(builder)
if result.is_ok():
if "error" in result.result_data:
return SuiRpcResult(False, result.result_data["error"]["message"], None)
# print(json.dumps(result.result_data["result"], indent=2))
result = SuiRpcResult(True, None, builder.handle_return(result.result_data["result"]))
return result
result = self._execute(builder)
if result.is_ok() and "error" not in result.result_data:
result = SuiRpcResult(True, None, builder.handle_return(result.result_data["result"]))
elif "error" in result.result_data:
msg = result.result_data["error"]["message"]
if msg == _ClientMixin._SIGNATURE_ERROR and recovery_id > 0:
result = SuiRpcResult(False, msg)
elif msg == _ClientMixin._SIGNATURE_ERROR and recovery_id == 0:
result = _inner_sign(1)
else:
result = SuiRpcResult(False, msg)
return result

return _inner_sign(_recovery_id)

def dry_run(self, builder: SuiBaseBuilder) -> Union[SuiRpcResult, Exception]:
"""Submit transaction than sui_dryRunTransaction only."""
Expand Down
33 changes: 15 additions & 18 deletions pysui/sui/sui_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@


class SuiPublicKey(PublicKey):
"""."""
"""SuiPublicKey Sui Basic public key."""

@property
def pub_key(self) -> str:
Expand All @@ -59,33 +59,35 @@ def pub_key(self) -> str:


class SuiPrivateKey(PrivateKey):
"""."""
"""SuiPrivateKey Sui Basic private/signing key."""

def sign_secure(self, public_key: SuiPublicKey, tx_data: str) -> bytes:
def sign_secure(self, public_key: SuiPublicKey, tx_data: str, recovery_id: int = 0) -> bytes:
"""sign_secure Sign transaction intent.
:param public_key: PublicKey from signer/private key
:type public_key: SuiPublicKey
:param tx_data: Transaction bytes being signed
:type tx_data: str
:param recovery_id: value used for secp256r1 signature completion,default to 0
:type: recovery_id: int, optional
:return: Singed transaction as bytes
:rtype: bytes
"""
indata = bytearray([0, 0, 0])
dec_tx = base64.b64decode(tx_data)
indata.extend(dec_tx)
compound = bytearray([self.scheme])
sig_bytes = self.sign(bytes(indata))
sig_bytes = self.sign(bytes(indata), recovery_id)
compound.extend(sig_bytes)
compound.extend(public_key.key_bytes)
return bytes(compound)


class SuiKeyPair(KeyPair):
"""."""
"""SuiKeyPair Sui Basic keypair."""

def __init__(self) -> None:
"""."""
"""__init__ Default keypair initializer."""
self._scheme: SignatureScheme = None
self._private_key: SuiPrivateKey = None
self._public_key: SuiPublicKey = None
Expand All @@ -105,9 +107,9 @@ def scheme(self) -> SignatureScheme:
"""Get the keys scheme."""
return self._scheme

def new_sign_secure(self, tx_data: str) -> SuiSignature:
def new_sign_secure(self, tx_data: str, recovery_id: int = 0) -> SuiSignature:
"""New secure sign with intent."""
sig = self.private_key.sign_secure(self.public_key, tx_data)
sig = self.private_key.sign_secure(self.public_key, tx_data, recovery_id)
return SuiSignature(base64.b64encode(sig).decode())

def serialize(self) -> str:
Expand Down Expand Up @@ -153,16 +155,11 @@ def __init__(self, indata: bytes) -> None:
raise SuiInvalidKeyPair(f"Private Key expects {SECP256R1_PRIVATEKEY_BYTES_LEN} bytes, found {dlen}")
super().__init__(SignatureScheme.SECP256R1, indata)
self._signing_key = ecdsa.SigningKey.from_string(indata, ecdsa.NIST256p, hashfunc=hashlib.sha256)
self._pad_byte = int(1).to_bytes(1, "little")

# FIXME: Need to understand where the recovery ID comes from
def sign(self, data: bytes) -> bytes:
def sign(self, data: bytes, recovery_id: int = 0) -> bytes:
"""SECP256R1 sign data bytes."""
core_sig = self._signing_key.sign_deterministic(data, hashfunc=hashlib.sha256)
print(f"Sig size = {len(core_sig)}")
print(f"Last byte = {core_sig[-1]}")
core_sig += self._pad_byte
# core_sig[-1].to_bytes(1, "little") # data[-1].to_bytes(1, "little") # self._pad_byte
core_sig += recovery_id.to_bytes(1, "little")
return core_sig


Expand Down Expand Up @@ -217,7 +214,7 @@ def __init__(self, indata: bytes) -> None:
super().__init__(SignatureScheme.ED25519, indata)
self._signing_key = SigningKey(self.to_b64(), encoder=Base64Encoder)

def sign(self, data: bytes) -> bytes:
def sign(self, data: bytes, _recovery_id: int = 0) -> bytes:
"""ED25519 sign data bytes."""
sig = self._signing_key.sign(data, encoder=RawEncoder).signature
return sig
Expand Down Expand Up @@ -253,7 +250,7 @@ def from_bytes(cls, indata: bytes) -> KeyPair:


# Secp256
# TODO: Change to use the ecdsa library
# TODO: Change to use the ecdsa library and drop the secp256k1 library requirement


class SuiPublicKeySECP256K1(SuiPublicKey):
Expand All @@ -278,7 +275,7 @@ def __init__(self, indata: bytes) -> None:
super().__init__(SignatureScheme.SECP256K1, indata)
self._signing_key = secp256k1.PrivateKey(indata, raw=True)

def sign(self, data: bytes) -> bytes:
def sign(self, data: bytes, _recovery_id: int = 0) -> bytes:
"""secp256k1 sign data bytes."""
sig = self._signing_key.ecdsa_sign_recoverable(data)
sig_sb, sig_si = self._signing_key.ecdsa_recoverable_serialize(sig)
Expand Down

0 comments on commit 8ae11bb

Please sign in to comment.