Skip to content

Commit

Permalink
add multi sign support(fix crypto-org-chain#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
linfeng-crypto committed Feb 14, 2022
1 parent c4f7c12 commit 4b36eca
Show file tree
Hide file tree
Showing 12 changed files with 727 additions and 11 deletions.
96 changes: 96 additions & 0 deletions chainlibpy/amino/transaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import base64
import hashlib
import json
from typing import List

import ecdsa

from chainlibpy.amino import StdFee, StdSignDoc, SyncMode
from chainlibpy.amino.message import Msg
from chainlibpy.amino.tx import Pubkey, Signature, StdTx
from chainlibpy.wallet import Wallet


class Transaction:
"""A Cosmos transaction.
After initialization, one or more token transfers can be added by
calling the `add_transfer()` method. Finally, call `get_pushable()`
to get a signed transaction that can be pushed to the `POST /txs`
endpoint of the Cosmos REST API.
"""

def __init__(
self,
wallet: Wallet,
account_num: int,
sequence: int,
fee: StdFee = StdFee.default(),
memo: str = "",
chain_id: str = "crypto-org-chain-mainnet-1",
sync_mode: SyncMode = "sync",
timeout_height: int = 0,
multi_sign_address: str = None,
) -> None:
self._multi_sign_address = multi_sign_address
self._wallet = wallet
self._account_num = str(account_num)
self._sequence = str(sequence)
self._fee = fee
self._memo = memo
self._chain_id = chain_id
self._sync_mode = sync_mode
self._msgs: List[Msg] = []
self._timeout_height = str(timeout_height)

def add_msg(self, msg: Msg):
self._msgs.append(msg)

def get_pushable(self) -> dict:
"""get the request post to the /txs."""
std_tx = StdTx(self._msgs, self._fee, self._memo, self._timeout_height, [self.signature])

pushable_tx = {
"tx": std_tx.to_dict(),
"mode": self._sync_mode,
}
return pushable_tx

@property
def signature(self) -> Signature:
pubkey = self._wallet.public_key
base64_pubkey = base64.b64encode(pubkey).decode("utf-8")
pubkey = Pubkey(value=base64_pubkey)
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) -> bytes:
sign_doc = self._get_sign_doc()
message_str = json.dumps(
sign_doc.to_dict(), separators=(",", ":"), sort_keys=True
)
message_bytes = message_str.encode("utf-8")

privkey = ecdsa.SigningKey.from_string(
self._wallet.private_key, curve=ecdsa.SECP256k1
)
signature_compact = privkey.sign_deterministic(
message_bytes,
hashfunc=hashlib.sha256,
sigencode=ecdsa.util.sigencode_string_canonize,
)
return signature_compact

def _get_sign_doc(self) -> StdSignDoc:
sign_doc = StdSignDoc(
account_number=self._account_num,
sequence=self._sequence,
chain_id=self._chain_id,
memo=self._memo,
fee=self._fee.to_dict(),
msgs=self._msgs,
timeout_height=self._timeout_height,
)
return sign_doc
85 changes: 85 additions & 0 deletions chainlibpy/generated/cosmos/crypto/multisig/keys_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions chainlibpy/generated/cosmos/crypto/multisig/keys_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

11 changes: 10 additions & 1 deletion chainlibpy/grpc_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ class NetworkConfig:
exponent=8,
derivation_path="m/44'/1'/0'/0/0",
),
"devnet": NetworkConfig(
grpc_endpoint="0.0.0.0:26653",
chain_id="chain_id",
address_prefix="cro",
coin_denom="cro",
coin_base_denom="basecro",
derivation_path="m/44'/394'/0'/0/0",
exponent=8,
),
}


Expand Down Expand Up @@ -160,4 +169,4 @@ def broadcast_transaction(
else:
raise TypeError("Unexcepted mode, should be [sync, async, block]")

self.tx_client.BroadcastTx(BroadcastTxRequest(tx_bytes=tx_byte, mode=_mode))
return self.tx_client.BroadcastTx(BroadcastTxRequest(tx_bytes=tx_byte, mode=_mode))
Empty file.
74 changes: 74 additions & 0 deletions chainlibpy/multisign/bitarray.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import base64

from chainlibpy.amino.basic import BasicObj
from chainlibpy.generated.cosmos.crypto.multisig.v1beta1.multisig_pb2 import (
CompactBitArray as ProtoCompactBitArray,
)
from chainlibpy.multisign.bits import Bits


class CompactBitArray(BasicObj):
MaxInt32 = 2147483647

def __init__(self, bits: int):
self.extra_bits_stored = 0
self.elems = bytearray()
if bits <= 0:
raise Exception(f"invalid bits {bits}")
n_elems = (bits + 7) // 8
if n_elems <= 0 or n_elems > self.MaxInt32:
raise Exception(f"invalid bits {bits}")
self.extra_bits_stored = bits % 8
self.elems = bytearray([0] * n_elems)

def __repr__(self):
elems = base64.b64encode(self.elems).decode('utf-8')
return f"extra_bits_stored:{self.extra_bits_stored}, elems:{elems}"

def bit_array(self) -> ProtoCompactBitArray:
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."""
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()
"""
if index < 0 or index >= self.count():
return False
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()
"""
if i < 0 or i >= self.count():
return False

if v:
self.elems[i >> 3] |= (1 << (7 - (i % 8)))
else:
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
27 changes: 27 additions & 0 deletions chainlibpy/multisign/bits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class Bits(object):
pop8tab = [
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])
Loading

0 comments on commit 4b36eca

Please sign in to comment.