diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 626a2b1..e26ac6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -5,6 +5,7 @@ on: push: branches: - master + - multi-sig2 jobs: tox_test: diff --git a/README.md b/README.md index 2794c78..b12bf63 100644 --- a/README.md +++ b/README.md @@ -65,12 +65,12 @@ CHAIN_ID = "chainmaind" GRPC_ENDPOINT = "0.0.0.0:26653" wallet = Wallet(MNEMONIC_PHRASE) -client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT) +client = GrpcClient(CHAIN_ID, DENOM, GRPC_ENDPOINT) from_address = wallet.address account_number = client.query_account_data(wallet.address).account_number -msg = client.get_packed_send_msg(wallet.address, TO_ADDRESS, AMOUNT) +msg = get_packed_send_msg(wallet.address, TO_ADDRESS, AMOUNT) tx = client.generate_tx([msg], [wallet.address], [wallet.public_key]) sign_transaction(tx, wallet.private_key, CHAIN_ID, account_number) client.broadcast_tx(tx) diff --git a/chainlibpy/amino/transaction.py b/chainlibpy/amino/transaction.py index 29e1092..85c7f0d 100644 --- a/chainlibpy/amino/transaction.py +++ b/chainlibpy/amino/transaction.py @@ -10,6 +10,7 @@ from chainlibpy.amino.tx import Pubkey, Signature, StdTx from chainlibpy.wallet import Wallet + class Transaction: """A Cosmos transaction. @@ -60,10 +61,12 @@ def signature(self) -> Signature: pubkey = self._wallet.public_key base64_pubkey = base64.b64encode(pubkey).decode("utf-8") pubkey = Pubkey(value=base64_pubkey) - signature = Signature(self._sign(), pubkey, self._account_num, self._sequence) + raw_signature = self.sign() + sig_str = base64.b64encode(raw_signature).decode("utf-8") + signature = Signature(sig_str, pubkey, self._account_num, self._sequence) return signature - def _sign(self) -> str: + def sign(self) -> bytes: sign_doc = self._get_sign_doc() message_str = json.dumps( sign_doc.to_dict(), separators=(",", ":"), sort_keys=True @@ -78,10 +81,7 @@ def _sign(self) -> str: hashfunc=hashlib.sha256, sigencode=ecdsa.util.sigencode_string_canonize, ) - - print(list(signature_compact)) - signature_base64_str = base64.b64encode(signature_compact).decode("utf-8") - return signature_base64_str + return signature_compact def _get_sign_doc(self) -> StdSignDoc: sign_doc = StdSignDoc( @@ -93,4 +93,4 @@ def _get_sign_doc(self) -> StdSignDoc: msgs=self._msgs, timeout_height=self._timeout_height, ) - return sign_doc \ No newline at end of file + return sign_doc diff --git a/chainlibpy/grpc_client.py b/chainlibpy/grpc_client.py index 88a6007..a2a08f3 100644 --- a/chainlibpy/grpc_client.py +++ b/chainlibpy/grpc_client.py @@ -7,8 +7,6 @@ from google.protobuf.any_pb2 import Any as ProtoAny from grpc import ChannelCredentials, insecure_channel, secure_channel -from chainlibpy.generated.cosmos.crypto.multisig.keys_pb2 import LegacyAminoPubKey -from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import CompactBitArray, MultiSignature from chainlibpy.generated.cosmos.auth.v1beta1.auth_pb2 import BaseAccount from chainlibpy.generated.cosmos.auth.v1beta1.query_pb2 import QueryAccountRequest from chainlibpy.generated.cosmos.auth.v1beta1.query_pb2_grpc import ( @@ -23,6 +21,13 @@ ) from chainlibpy.generated.cosmos.bank.v1beta1.tx_pb2 import MsgSend from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin +from chainlibpy.generated.cosmos.crypto.multisig.keys_pb2 import LegacyAminoPubKey +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + CompactBitArray as ProtoCompactBitArray, +) +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + MultiSignature, +) from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey from chainlibpy.generated.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode from chainlibpy.generated.cosmos.tx.v1beta1.service_pb2 import ( @@ -42,20 +47,43 @@ Tx, TxBody, ) -from chainlibpy.transaction import sign_transaction from chainlibpy.multisign.signature import MultiSignatureData, SingleSignatureV2 +from chainlibpy.transaction import sign_transaction + +def get_packed_send_msg(from_address: str, to_address: str, amount: List[Coin]) -> ProtoAny: + msg_send = MsgSend(from_address=from_address, to_address=to_address, amount=amount) + send_msg_packed = ProtoAny() + send_msg_packed.Pack(msg_send, type_url_prefix="/") + + return send_msg_packed + + +def _get_signer_info(from_acc: BaseAccount, pub_key: ProtoPubKey) -> SignerInfo: + from_pub_key_packed = ProtoAny() + from_pub_key_pb = ProtoPubKey(key=pub_key) + from_pub_key_packed.Pack(from_pub_key_pb, type_url_prefix="/") + + # Prepare auth info + single = ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT) + mode_info = ModeInfo(single=single) + signer_info = SignerInfo( + public_key=from_pub_key_packed, + mode_info=mode_info, + sequence=from_acc.sequence, + ) + return signer_info class GrpcClient: DEFAULT_GAS_LIMIT = 200000 def __init__( - self, - chain_id: str, - grpc_endpoint: str, - account_number: int = None, - credentials: ChannelCredentials = None, + self, + chain_id: str, + denom: str, + grpc_endpoint: str, + credentials: ChannelCredentials = None, ) -> None: if credentials is None: channel = insecure_channel(grpc_endpoint) @@ -66,10 +94,10 @@ def __init__( self.tx_client = TxGrpcClient(channel) self.auth_client = AuthGrpcClient(channel) self.chain_id = chain_id - self.account_number = account_number + self.denom = denom - def get_balance(self, address: str, denom: str) -> QueryBalanceResponse: - res = self.bank_client.Balance(QueryBalanceRequest(address=address, denom=denom)) + def get_balance(self, address: str) -> QueryBalanceResponse: + res = self.bank_client.Balance(QueryBalanceRequest(address=address, denom=self.denom)) return res def query_account_data(self, address: str) -> BaseAccount: @@ -82,20 +110,20 @@ def query_account_data(self, address: str) -> BaseAccount: return account def generate_tx( - self, - packed_msgs: List[ProtoAny], - from_addresses: List[str], - pub_keys: List[bytes], - fee: Optional[List[Coin]] = None, - memo: str = "", - gas_limit: int = DEFAULT_GAS_LIMIT, + self, + packed_msgs: List[ProtoAny], + from_addresses: List[str], + pub_keys: List[bytes], + fee: Optional[List[Coin]] = None, + memo: str = "", + gas_limit: int = DEFAULT_GAS_LIMIT, ) -> Tx: accounts: List[BaseAccount] = [] signer_infos: List[SignerInfo] = [] for from_address, pub_key in zip(from_addresses, pub_keys): account = self.query_account_data(from_address) accounts.append(account) - signer_infos.append(self._get_signer_info(account, pub_key)) + signer_infos.append(_get_signer_info(account, pub_key)) auth_info = AuthInfo( signer_infos=signer_infos, @@ -109,15 +137,8 @@ def generate_tx( tx = Tx(body=tx_body, auth_info=auth_info) return tx - def sign_tx(self, private_key: bytes, tx: Tx): - sign_transaction(tx, private_key, self.chain_id, self.account_number) - - def get_packed_send_msg(self, from_address: str, to_address: str, amount: List[Coin]) -> ProtoAny: - msg_send = MsgSend(from_address=from_address, to_address=to_address, amount=amount) - send_msg_packed = ProtoAny() - send_msg_packed.Pack(msg_send, type_url_prefix="/") - - return send_msg_packed + def sign_tx(self, private_key: bytes, tx: Tx, account_number): + sign_transaction(tx, private_key, self.chain_id, account_number) def broadcast_tx(self, tx: Tx, wait_time: int = 10) -> GetTxResponse: tx_data = tx.SerializeToString() @@ -135,38 +156,36 @@ def broadcast_tx(self, tx: Tx, wait_time: int = 10) -> GetTxResponse: return tx_response - def bank_send(self, from_address: str, public_key: bytes, to_address: str, amount: List[Coin]) -> GetTxResponse: - import pdb; pdb.set_trace() - msg = self.get_packed_send_msg( + def bank_send( + self, + from_address: str, + private_key: bytes, + public_key: bytes, + to_address: str, + amount: List[Coin] + ) -> GetTxResponse: + account_info = self.query_account_data(from_address) + msg = get_packed_send_msg( from_address=from_address, to_address=to_address, amount=amount ) tx = self.generate_tx([msg], [from_address], [public_key]) - self.sign_tx(tx) + self.sign_tx(private_key, tx, account_info.account_number) return self.broadcast_tx(tx) - def _get_signer_info(self, from_acc: BaseAccount, pub_key: ProtoPubKey) -> SignerInfo: - from_pub_key_packed = ProtoAny() - from_pub_key_pb = ProtoPubKey(key=pub_key) - from_pub_key_packed.Pack(from_pub_key_pb, type_url_prefix="/") - - # Prepare auth info - single = ModeInfo.Single(mode=SignMode.SIGN_MODE_DIRECT) - mode_info = ModeInfo(single=single) - signer_info = SignerInfo( - public_key=from_pub_key_packed, - mode_info=mode_info, - sequence=from_acc.sequence, - ) - return signer_info -def get_muli_signer_info(sequence: int, multi_pubkey: LegacyAminoPubKey, bitarray: CompactBitArray) -> SignerInfo: +def get_muli_signer_info( + sequence: int, + multi_pubkey: LegacyAminoPubKey, + bitarray: ProtoCompactBitArray +) -> SignerInfo: multi_pubkey_packed = ProtoAny() multi_pubkey_packed.Pack(multi_pubkey, type_url_prefix="/") # Prepare auth info - mode_infos = [ModeInfo.Single(mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON)] * multi_pubkey.threshold - multi = ModeInfo.Multi(bitarray, mode_infos) + signal_mode_info = ModeInfo(single=ModeInfo.Single(mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON)) + mode_infos = [signal_mode_info] * multi_pubkey.threshold + multi = ModeInfo.Multi(bitarray=bitarray, mode_infos=mode_infos) mode_info = ModeInfo(multi=multi) signer_info = SignerInfo( public_key=multi_pubkey_packed, @@ -175,6 +194,7 @@ def get_muli_signer_info(sequence: int, multi_pubkey: LegacyAminoPubKey, bitarra ) return signer_info + def gen_multi_tx( packed_msgs: List[ProtoAny], multi_pubkey: LegacyAminoPubKey, @@ -184,18 +204,17 @@ def gen_multi_tx( memo: str = "", gas_limit: int = 2000, ) -> Tx: - def packed_proto_pubkey(pub_key: ProtoPubKey): - from_pub_key_packed = ProtoAny() - return from_pub_key_packed.Pack(pub_key, type_url_prefix="/") + if multi_pubkey.threshold > len(signature_batch): + raise Exception("single signatures should >= threshold") all_proto_pubkeys = [p for p in multi_pubkey.public_keys] n = len(signature_batch) multi_sign_data = MultiSignatureData(n) for sig_v2 in signature_batch: - multi_sign_data.add_signature_v2(sig_v2, all_proto_pubkeys) + multi_sign_data.add_single_sig_v2(sig_v2, all_proto_pubkeys) signer_info = get_muli_signer_info(sequence, multi_pubkey, multi_sign_data.bit_array) - signer_infos: List[SignerInfo] = [] + signer_infos = list() signer_infos.append(signer_info) auth_info = AuthInfo( signer_infos=signer_infos, @@ -206,8 +225,6 @@ def packed_proto_pubkey(pub_key: ProtoPubKey): tx_body.memo = memo tx_body.messages.extend(packed_msgs) multi_signature = MultiSignature(signatures=multi_sign_data.signatures) - signatures = list() - signatures.append(multi_signature) - packed_pubkeys = [packed_proto_pubkey(p.pub_key) for p in signature_batch] - tx = Tx(body=tx_body, auth_info=auth_info, public_keys=packed_pubkeys, signatures=signatures) + + tx = Tx(body=tx_body, auth_info=auth_info, signatures=[multi_signature.SerializeToString()]) return tx diff --git a/chainlibpy/multisign/bitarray.py b/chainlibpy/multisign/bitarray.py index 8831e14..d6f58a3 100644 --- a/chainlibpy/multisign/bitarray.py +++ b/chainlibpy/multisign/bitarray.py @@ -1,6 +1,10 @@ import base64 + from chainlibpy.amino.basic import BasicObj -from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import CompactBitArray as ProtoCompactBitArray +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + CompactBitArray as ProtoCompactBitArray, +) +from chainlibpy.multisign.bits import Bits class CompactBitArray(BasicObj): @@ -22,35 +26,49 @@ def __repr__(self): return f"extra_bits_stored:{self.extra_bits_stored}, elems:{elems}" def bit_array(self) -> ProtoCompactBitArray: - return ProtoCompactBitArray(extra_bits_storede=self.extra_bits_stored, elems=self.elems) + return ProtoCompactBitArray( + extra_bits_stored=self.extra_bits_stored, + elems=bytes(self.elems) + ) def count(self) -> int: - """ - returns the number of bits in the bitarray - """ + """returns the number of bits in the bitarray.""" if self.extra_bits_stored == 0: return len(self.elems) * 8 return (len(self.elems) - 1) * 8 + int(self.extra_bits_stored) def get_index(self, index: int) -> bool: + """returns the bit at index i within the bit array. + + The behavior is undefined if i >= self.count() """ - returns the bit at index i within the bit array. The behavior is undefined if i >= bA.Count() - """ - if index > self.count() or index < 0: + if index < 0 or index >= self.count(): return False - return self.elems[index >> 3] & (1 << (7 - (index % 8))) > 0 + return (self.elems[index >> 3] & (1 << (7 - (index % 8)))) > 0 def set_index(self, i: int, v: bool) -> bool: + """set_index sets the bit at index i within the bit array. + + Returns true if and only if the operation succeeded. The + behavior is undefined if i >= self.count() """ - set_index sets the bit at index i within the bit array. Returns true if and only if the - operation succeeded. The behavior is undefined if i >= self.count() - """ - if i > self.count() or i < 0: + if i < 0 or i >= self.count(): return False - index = i >> 3 if v: - self.elems[index] |= (1 << int(7 - (i % 8))) + self.elems[i >> 3] |= (1 << (7 - (i % 8))) else: - self.elems[i >> 3] &= ~(1 << int(7 - (i % 8))) + self.elems[i >> 3] &= ~(1 << (7 - (i % 8))) return True + + def num_true_bits_before(self, index: int) -> int: + ones_count = 0 + max_ = self.count() + index = min(index, max_) + elem = 0 + while True: + if elem*8+7 >= index: + ones_count += Bits.ones_count8(self.elems[elem]) >> (7 - (index % 8) + 1) + return ones_count + ones_count += Bits.ones_count8(self.elems[elem]) + elem += 1 diff --git a/chainlibpy/multisign/bits.py b/chainlibpy/multisign/bits.py new file mode 100644 index 0000000..3924c21 --- /dev/null +++ b/chainlibpy/multisign/bits.py @@ -0,0 +1,27 @@ +class Bits(object): + po8tab = [ + 0x00, 0x01, 0x01, 0x02, 0x01, 0x02, 0x02, 0x03, 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x01, 0x02, 0x02, 0x03, 0x02, 0x03, 0x03, 0x04, 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x02, 0x03, 0x03, 0x04, 0x03, 0x04, 0x04, 0x05, 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x03, 0x04, 0x04, 0x05, 0x04, 0x05, 0x05, 0x06, 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, + 0x04, 0x05, 0x05, 0x06, 0x05, 0x06, 0x06, 0x07, 0x05, 0x06, 0x06, 0x07, 0x06, 0x07, 0x07, 0x08 + ] + + @classmethod + def ones_count8(cls, x: int) -> int: + """returns the number of one bits ("population count") in x. + + from: go/match/bits/bits.go + """ + return int(cls.pop8tab[x]) diff --git a/chainlibpy/multisign/signature.py b/chainlibpy/multisign/signature.py index d28cf83..aaf1344 100644 --- a/chainlibpy/multisign/signature.py +++ b/chainlibpy/multisign/signature.py @@ -1,11 +1,22 @@ import base64 -from typing import List from dataclasses import dataclass +from typing import List + +from google.protobuf.any_pb2 import Any as ProtoAny + from chainlibpy.amino.basic import BasicObj -from chainlibpy.multisign.bitarray import CompactBitArray +from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import ( + CompactBitArray as ProtoCompactBitArray, +) from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey from chainlibpy.generated.cosmos.tx.signing.v1beta1.signing_pb2 import SignMode -from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import CompactBitArray as ProtoCompactBitArray +from chainlibpy.multisign.bitarray import CompactBitArray + + +def packed_to_any(m): + packed = ProtoAny() + packed.Pack(m, type_url_prefix="/") + return packed @dataclass(init=True, repr=True, eq=True, order=True, frozen=True) @@ -21,34 +32,40 @@ class SingleSignatureV2(object): def __init__(self, pub_key: bytes, sequence: int, raw_sig: bytes): self.pub_key = ProtoPubKey(key=pub_key) - self.data = SingleSignatureData(sign_mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON, signature=raw_sig) + self.data = SingleSignatureData( + sign_mode=SignMode.SIGN_MODE_LEGACY_AMINO_JSON, + signature=raw_sig + ) self.sequence = sequence class MultiSignatureData(object): - bit_array: CompactBitArray - signatures: List[bytes] - def __init__(self, n: int): - self.bit_array = CompactBitArray(n) + self._bit_array = CompactBitArray(n) self.signatures = [] def __repr__(self): signatures = [base64.b64encode(s).decode('utf-8') for s in self.signatures] return f"signatures: {signatures}, bit_array: {self.bit_array}" + @property def bit_array(self) -> ProtoCompactBitArray: - self.bit_array.bit_array() + return self._bit_array.bit_array() - def add_signature_from_pubkey(self, sig: bytes, pubkey: ProtoPubKey, all_pubkeys: List[ProtoPubKey]): - index = all_pubkeys.index(pubkey) - new_sig_index = self.bit_array.num_true_bits_before(index) + def add_signature_from_pubkey( + self, + sig: bytes, + pubkey: ProtoPubKey, + all_pubkeys: List[ProtoPubKey] + ): + index = all_pubkeys.index(packed_to_any(pubkey)) + new_sig_index = self._bit_array.num_true_bits_before(index) # replace the old - if self.bit_array.get_index(index): + if self._bit_array.get_index(index): self.signatures[new_sig_index] = sig return - self.bit_array.set_index(index, True) + self._bit_array.set_index(index, True) # Optimization if the index is the greatest index if new_sig_index == len(self.signatures): @@ -57,8 +74,14 @@ def add_signature_from_pubkey(self, sig: bytes, pubkey: ProtoPubKey, all_pubkeys # insert at the new_sig_index self.signatures.insert(new_sig_index, sig) - # todo: remove this - print([list(i) for i in self.signatures]) - def add_single_signature(self, single_sig_v2: SingleSignatureV2, all_pubkeys: List[ProtoPubKey]): - self.add_signature_from_pubkey(single_sig_v2.data.signature, single_sig_v2.pub_key, all_pubkeys) + def add_single_sig_v2( + self, + single_sig_v2: SingleSignatureV2, + all_pubkeys: List[ProtoPubKey] + ): + self.add_signature_from_pubkey( + single_sig_v2.data.signature, + single_sig_v2.pub_key, + all_pubkeys + ) diff --git a/chainlibpy/test.py b/chainlibpy/test.py deleted file mode 100644 index 6c88964..0000000 --- a/chainlibpy/test.py +++ /dev/null @@ -1,29 +0,0 @@ -from generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import CompactBitArray, MultiSignature - -def test_multisig_data_encode(): - # bit_array = CompactBitArray(2) - msig = MultiSignatureData(2) - # bit_array.elems = bytearray([192]) - - sigs = [ - # "XYuiAJTf4yfnHg6SWA3/NzYULogroPGquZKSlXDW80w/jHJxzhU0NmPWlJFyKJeDsduovHPMG+c021Yw/hJxDg==", - # "ttB4rkFW/MwMXgv9dUjO5P8qEoFWMhepicEFzDcfB+Ic5HjTr+NfTn7x2XquJr/sEvYknJQK25BXmsnEmHhGEg==", - "ttB4rkFW/MwMXgv9dUjO5P8qEoFWMhepicEFzDcfB+Ic5HjTr+NfTn7x2XquJr/sEvYknJQK25BXmsnEmHhGEg==", - "XYuiAJTf4yfnHg6SWA3/NzYULogroPGquZKSlXDW80w/jHJxzhU0NmPWlJFyKJeDsduovHPMG+c021Yw/hJxDg==", - ] - # sigs = [base64.b64decode(i) for i in sigs] - for index, s in enumerate(sigs): - msig.add_signature(s, index) - for s in msig.signatures: - print(list(s)) - raw = msig.encode_amino_binary() - print("\n\nraw: ", list(raw)) - print(msig.encode_amino_base64()) - - -# bit_array = CompactBitArray(2) -# bit_array.set_index(0, True) -# bit_array.set_index(1, True) -# print(list(bit_array.elems)) -test_multisig_data_encode() - diff --git a/example/multi_sig/config.yaml b/example/multi_sig/config.yaml index b594128..6d86adf 100644 --- a/example/multi_sig/config.yaml +++ b/example/multi_sig/config.yaml @@ -7,10 +7,12 @@ chain_id_test: accounts: - name: msigner0 coins: 2000cro + mnemonic: hurry exist clerk safe aware anchor brush run dentist come surge frame tired economy school grief volcano enforce word alpha liar clever sure taxi - name: msigner1 coins: 2000cro - name: msigner2 coins: 2000cro + mnemonic: repeat life corn cliff tragic merry zoo saddle fuel shove column pulp decorate forward rabbit ocean agent snack gaze mansion when wood grab pear app_state: staking: params: diff --git a/example/multi_sig/main.py b/example/multi_sig/main.py index cdeed0e..419673c 100644 --- a/example/multi_sig/main.py +++ b/example/multi_sig/main.py @@ -1,22 +1,22 @@ -import json +import time from pathlib import Path + import requests import yaml -from pystarport.cluster import (ClusterCLI, find_account, init_cluster, - interact, start_cluster) +from google.protobuf.any_pb2 import Any as ProtoAny +from pystarport.cluster import ClusterCLI, init_cluster, interact, start_cluster from pystarport.ports import api_port from pystarport.proto_python.api_util import ApiUtil -from chainlibpy.wallet import Wallet -from chainlibpy.amino.transaction import Transaction + from chainlibpy.amino import Coin, StdFee from chainlibpy.amino.message import MsgSend -from chainlibpy.multisign.signature import SingleSignatureV2 -from chainlibpy.grpc_client import gen_multi_tx, GrpcClient -from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey -from chainlibpy.grpc_client import Coin as ProtoCoin +from chainlibpy.amino.transaction import Transaction +from chainlibpy.generated.cosmos.base.v1beta1.coin_pb2 import Coin as ProtoCoin from chainlibpy.generated.cosmos.crypto.multisig.keys_pb2 import LegacyAminoPubKey -from chainlibpy.amino.basic import DEFAULT_BECH32_HRP_BASE -from google.protobuf.any_pb2 import Any as ProtoAny +from chainlibpy.generated.cosmos.crypto.secp256k1.keys_pb2 import PubKey as ProtoPubKey +from chainlibpy.grpc_client import GrpcClient, gen_multi_tx, get_packed_send_msg +from chainlibpy.multisign.signature import SingleSignatureV2 +from chainlibpy.wallet import Wallet def packed_to_any(m): @@ -25,15 +25,16 @@ def packed_to_any(m): return packed -class ChainMainClient(): +class ChainMainClient(object): ''' we use pystarport to create the IBC env need to install hermes: https://github.com/informalsystems/ibc-rs/releases ''' - def __init__(self, data_root=Path("/tmp/data"), config_file="config.yaml"): + + def __init__(self, data_root=Path("/tmp/data"), config_file="config.yaml", chain_id="chain_id_test"): self.data_root = data_root self.config_file = config_file - self.chain_id = "chain_id_test" + self.chain_id = chain_id @property def cluster(self): @@ -83,68 +84,79 @@ def start(self): ''' after start the tasks, you can use `supervisorctl -c task.ini` to see the status of each program ''' - data_path = "/tmp/dadta" - interact(f"rm -r {data_path}; mkdir -p {data_path}", ignore_error=True) - data_dir = Path("/tmp/data") + interact(f"rm -r {self.data_root}; mkdir -p {self.data_root}", ignore_error=True) + data_dir = Path(self.data_root) init_cluster(data_dir, "config.yaml", 26650) start_cluster(data_dir) -def find_account(data_dir, chain_id, name): - accounts = json.load((data_dir / chain_id / "accounts.json").open()) - return next(acct for acct in accounts if acct["name"] == name) -def get_multi_addr(multi_wallet_name: str): - r = ChainMainClient() - cluster = r.cluster - return cluster.address(multi_wallet_name) +# def get_multi_addr(multi_wallet_name: str): +# client = ChainMainClient() +# cluster = client.cluster +# import pdb; pdb.set_trace() +# return cluster.address(multi_wallet_name) -def prepare(chain_maind_client: ChainMainClient, multi_wallet_name: str): - """ - 1. create multi wallet - 2. send some coin to multi wallet - """ + +# the wallets which create the multi wallet +SEED_0 = "hurry exist clerk safe aware anchor brush run dentist come surge frame tired economy school grief volcano enforce word alpha liar clever sure taxi" +SEED_1 = "repeat life corn cliff tragic merry zoo saddle fuel shove column pulp decorate forward rabbit ocean agent snack gaze mansion when wood grab pear" +MULTI_WALLET = "multi_wallet" - multi_wallet_addr = get_multi_addr(multi_wallet_name) - balance = chain_maind_client.get_balance(multi_wallet_addr) +def prepare(multi_wallet_name: str): + """ + """ + # 1. create multi wallet + client = ChainMainClient() + cluster = client.cluster + cluster.make_multisig(multi_wallet_name, "msigner0", "msigner1") + multi_addr = cluster.address(multi_wallet_name) + + # 2. send some coin to multi wallet + wallet_0 = Wallet(SEED_0) + cluster.transfer(wallet_0.address, multi_addr, "500basecro") + time.sleep(10) + balance = client.get_balance(multi_addr) print("multi address balance: ", balance) + return multi_addr + def main(): chain_id = "chain_id_test" - multi_wallet_name = "multi_wallet" - chain_maind_client = ChainMainClient() - - seed_0 = find_account(chain_maind_client.data_root, chain_id, "msigner0")["mnemonic"] - print(f"seed0: {seed_0}") - return - seed_1 = find_account(chain_maind_client.data_root, chain_id, "msigner1")["mnemonic"] - wallet_0 = Wallet(seed_0) - wallet_1 = Wallet(seed_1) - to_address = "cro1hk220qwxp0c8m3pzazardmmfv8y0mg7ukdnn37" # wallet_0.address - - #print multi wallet balance - multi_wallet_addr = get_multi_addr(multi_wallet_name) - balance = chain_maind_client.get_balance(multi_wallet_addr) + wallet_0 = Wallet(SEED_0) + wallet_1 = Wallet(SEED_1) + chain_maind_client = ChainMainClient(chain_id=chain_id) + # multi_wallet_name = "multi_wallet" + # multi_address = prepare(multi_wallet_name) + multi_address = "cro15ejy7v88xarw5ce29t2rsve3kxn8d7ug44jayh" + to_address = "cro1hk220qwxp0c8m3pzazardmmfv8y0mg7ukdnn37" + + # print multi wallet balance + balance = chain_maind_client.get_balance(multi_address) print("multi address balance: ", balance) - fee = StdFee.default() - # account_num, sequence = 12, 0 - multi_wallet_addr = get_multi_addr(multi_wallet_name) - account_num, sequence = chain_maind_client.get_account_info(multi_wallet_addr) - print(f"account_num {account_num}, sequence: {sequence}") + fee = StdFee(gas="2000", amount=[]) + # multi_wallet_addr = get_multi_addr(multi_wallet_name) + # account_num, sequence = chain_maind_client.get_account_info(multi_wallet_addr) - grpc_client = GrpcClient(chain_id, "0.0.0.0:26653", account_num, None) + grpc_client = GrpcClient(chain_id, "basecro", "0.0.0.0:26653", None) + res = grpc_client.get_balance(multi_address) + print(f"get multi address balance: {res.balance.amount}") + account_info = grpc_client.query_account_data(multi_address) + account_num = account_info.account_num + sequence = account_info + print(f"account_num {account_num}, sequence: {sequence}") + # account_num, sequence = 12, 0 amount = "100" - msg = MsgSend(from_address=multi_wallet_addr, to_address=to_address, amount=[Coin(amount)]) + msg = MsgSend(from_address=multi_address, to_address=to_address, amount=[Coin(amount)]) # first make two single amino transaction, and create single_sig_v2 sig_batch = list() threshold = 2 - pubkeys = [packed_to_any(ProtoPubKey(key=w.public_key)) for w in [wallet_0, wallet_1]] - - multi_pubkey = LegacyAminoPubKey(threshold=threshold, public_keys=pubkeys) + any_pubkeys = [packed_to_any(ProtoPubKey(key=w.public_key)) for w in [wallet_0, wallet_1]] + multi_pubkey = LegacyAminoPubKey(threshold=threshold, public_keys=any_pubkeys) for index, wallet in enumerate([wallet_0, wallet_1]): tx = Transaction( wallet=wallet, @@ -152,15 +164,14 @@ def main(): sequence=sequence, chain_id=chain_id, fee=fee, - multi_sign_address=multi_wallet_addr + multi_sign_address=multi_address ) tx.add_msg(msg) - single_sig_v2 = SingleSignatureV2(wallet.public_key, sequence, tx.signature) + single_sig_v2 = SingleSignatureV2(wallet.public_key, sequence, tx.sign()) sig_batch.append(single_sig_v2) - import pdb; pdb.set_trace() amount = [ProtoCoin(amount="10000", denom="basecro")] - msg = grpc_client.get_packed_send_msg(multi_wallet_addr, to_address, amount) + msg = get_packed_send_msg(multi_address, to_address, amount) tx = gen_multi_tx( [msg], multi_pubkey, @@ -170,5 +181,5 @@ def main(): response = grpc_client.broadcast_tx(tx) print(f"send tx response: {response}") - -main() \ No newline at end of file +if __name__ == "__main__": + main() diff --git a/example/secure_channel_example.py b/example/secure_channel_example.py index 323923f..39755a6 100644 --- a/example/secure_channel_example.py +++ b/example/secure_channel_example.py @@ -30,10 +30,10 @@ def example_with_certificate_file(): with open("./cert.crt", "rb") as f: creds = grpc.ssl_channel_credentials(f.read()) - client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT, creds) + client = GrpcClient(CHAIN_ID, DENOM, GRPC_ENDPOINT, creds) from_address = wallet.address - res = client.get_balance(from_address, DENOM) + res = client.get_balance(from_address) print(f"address {from_address} initial balance: {res.balance.amount}") @@ -48,10 +48,10 @@ def example_with_certificate_request(): certificate = ssl.DER_cert_to_PEM_cert(sock.getpeercert(True)) creds = grpc.ssl_channel_credentials(str.encode(certificate)) - client = GrpcClient(wallet, CHAIN_ID, GRPC_ENDPOINT, creds) + client = GrpcClient(CHAIN_ID, DENOM, GRPC_ENDPOINT, creds) from_address = wallet.address - res = client.get_balance(from_address, DENOM) + res = client.get_balance(from_address) print(f"address {from_address} initial balance: {res.balance.amount}") diff --git a/example/transaction.py b/example/transaction.py index ec26bc9..1c9f62f 100644 --- a/example/transaction.py +++ b/example/transaction.py @@ -15,7 +15,7 @@ MNEMONIC_PHRASE = "hurry exist clerk safe aware anchor brush run dentist come surge frame tired economy school grief volcano enforce word alpha liar clever sure taxi" # Obtained from {directory_started_pystarport}/data/chainmaind/accounts.json # Another address to receive coins sent -TO_ADDRESS = "cro1hk220qwxp0c8m3pzazardmmfv8y0mg7ukdnn38" +TO_ADDRESS = "cro1hk220qwxp0c8m3pzazardmmfv8y0mg7ukdnn37" AMOUNT = [Coin(amount="10000", denom=DENOM)] # Obtained from {directory_started_pystarport}/data/chainmaind/genesis.json CHAIN_ID = "chain_id_test" @@ -26,20 +26,19 @@ def main(): wallet = Wallet(MNEMONIC_PHRASE) - client = GrpcClient(CHAIN_ID, GRPC_ENDPOINT) + client = GrpcClient(CHAIN_ID, DENOM, GRPC_ENDPOINT) from_address = wallet.address - res = client.get_balance(from_address, DENOM) + res = client.get_balance(from_address) print(f"from_address initial balance: {res.balance.amount}") # res = client.get_balance(TO_ADDRESS, DENOM) # print(f"to_address initial balance: {res.balance.amount}") - - client.bank_send(wallet.address, wallet.public_key, TO_ADDRESS, AMOUNT) + client.bank_send(wallet.address, wallet.private_key, wallet.public_key, TO_ADDRESS, AMOUNT) print("after successful transaction") - res = client.get_balance(from_address, DENOM) + res = client.get_balance(from_address) print(f"from_address updated balance: {res.balance.amount}") - res = client.get_balance(TO_ADDRESS, DENOM) + res = client.get_balance(TO_ADDRESS) print(f"to_address updated balance: {res.balance.amount}") diff --git a/tests/test_grpc_client.py b/tests/test_grpc_client.py index 2e92667..e648800 100644 --- a/tests/test_grpc_client.py +++ b/tests/test_grpc_client.py @@ -30,7 +30,6 @@ add_ServiceServicer_to_server as add_tx, ) from chainlibpy.grpc_client import GrpcClient -from chainlibpy.wallet import Wallet class MockBankQueryServicer(BankQueryServicer): @@ -54,9 +53,6 @@ class MockTxServiceServicer(TxServiceServicer): @pytest.fixture(scope="module") def mock_grpc_client(_grpc_server, grpc_addr): - seed = "burst negative solar evoke traffic yard lizard next series foster seminar enter wrist captain bulb trap giggle country sword season shoot boy bargain deal" # noqa 501 - wallet = Wallet(seed) - add_bank(MockBankQueryServicer(), _grpc_server) add_auth(MockAuthQueryServicer(), _grpc_server) add_tx(MockTxServiceServicer(), _grpc_server) @@ -64,10 +60,10 @@ def mock_grpc_client(_grpc_server, grpc_addr): _grpc_server.add_insecure_port(grpc_addr) _grpc_server.start() - yield GrpcClient(wallet, "chainmaind", grpc_addr) + yield GrpcClient("chainmaind", "basecro", grpc_addr) _grpc_server.stop(grace=None) def test_get_balance(mock_grpc_client): - balance = mock_grpc_client.get_balance("cro1yj3fd8gxrqd2662p8ywp26t4hfws9p5n75xjum", "basecro") + balance = mock_grpc_client.get_balance("cro1yj3fd8gxrqd2662p8ywp26t4hfws9p5n75xjum") assert balance == QueryBalanceResponse(balance=Coin(amount="100000000", denom="basecro")) diff --git a/tox.ini b/tox.ini index 434f074..e897659 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,8 @@ minversion = 3.3.0 # arguments use the pyproject.toml file as specified in PEP-517 and PEP-518. isolated_build = true +[flake8] +max-line-length = 120 [testenv] whitelist_externals = poetry