From 717b63fbe433dad965d1e8afbe9f3444a2f1be5e Mon Sep 17 00:00:00 2001
From: Moul <moul@moul.re>
Date: Tue, 22 Jun 2021 21:41:54 +0200
Subject: [PATCH] [mod] #173: Make Transaction handy with single and multi-sign

Store SigningKey and Signature arguments/attributes in Lists.
Allow to pass an instance or a list of SigningKey and pubkey
for single and multi signing
in Transaction.{__init__(),multi_sign(),check_signatures()}
if instance: store in a list

Overwrite Document.{sign(),check_signature()} as undefined.
Disable pylint error at Transaction class scope:
Raise Exception instead of NotImplentedError to not having to disable pylint:W0223
https://stackoverflow.com/a/22224042

Do not erase previous signatures when signing
To allow different key owner to sign additonally the Tx doc
---
 duniterpy/documents/transaction.py | 71 ++++++++++++++----------------
 examples/send_transaction.py       |  5 ++-
 tests/key/test_verifying_key.py    |  2 +-
 3 files changed, 36 insertions(+), 42 deletions(-)

diff --git a/duniterpy/documents/transaction.py b/duniterpy/documents/transaction.py
index 3e4e3ea1..21e4ba10 100644
--- a/duniterpy/documents/transaction.py
+++ b/duniterpy/documents/transaction.py
@@ -549,7 +549,7 @@ class Transaction(Document):
         outputs: List[OutputSource],
         comment: str,
         time: Optional[int] = None,
-        signing_key: SigningKey = None,
+        signing_keys: Optional[Union[SigningKey, List[SigningKey]]] = None,
         version: int = VERSION,
         currency: str = G1_CURRENCY_CODENAME,
     ) -> None:
@@ -564,7 +564,7 @@ class Transaction(Document):
         :param outputs: List of OutputSource instances
         :param comment: Comment field
         :param time: time when the transaction enters the blockchain
-        :param signing_key: SigningKey instance to sign the document (default=None)
+        :param signing_keys: SigningKey or list of SigningKey instances to sign the document (default=None)
         :param version: Document version (default=transaction.VERSION)
         :param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
         """
@@ -579,8 +579,8 @@ class Transaction(Document):
         self.time = time
         self.signatures: List[str] = list()
 
-        if signing_key is not None:
-            self.sign(signing_key)
+        if signing_keys:
+            self.multi_sign(signing_keys)
 
     def __eq__(self, other: Any) -> bool:
         """
@@ -591,7 +591,6 @@ class Transaction(Document):
         return (
             self.version == other.version
             and self.currency == other.currency
-            and self.signature == other.signature
             and self.signatures == other.signatures
             and self.blockstamp == other.blockstamp
             and self.locktime == other.locktime
@@ -608,7 +607,6 @@ class Transaction(Document):
             (
                 self.version,
                 self.currency,
-                self.signature,
                 self.signatures,
                 self.blockstamp,
                 self.locktime,
@@ -744,10 +742,7 @@ Comment: {comment}
         )
 
         # return transaction with signatures
-        if len(signatures) > 1:
-            transaction.signatures = signatures
-        else:
-            transaction.signature = signatures[0]
+        transaction.signatures = signatures
         return transaction
 
     @classmethod
@@ -837,11 +832,7 @@ Comment: {comment}
         )
 
         # return transaction with signatures
-        if len(signatures) > 1:
-            transaction.signatures = signatures
-        else:
-            transaction.signature = signatures[0]
-
+        transaction.signatures = signatures
         return transaction
 
     def raw(self) -> str:
@@ -917,11 +908,8 @@ Currency: {1}
             doc += "{0}\n".format(o.inline())
         if self.comment != "":
             doc += "{0}\n".format(self.comment)
-        if self.signature is not None:
-            doc += "{0}\n".format(self.signature)
-        else:
-            for signature in self.signatures:
-                doc += "{0}\n".format(signature)
+        for signature in self.signatures:
+            doc += "{0}\n".format(signature)
 
         return doc
 
@@ -931,44 +919,49 @@ Currency: {1}
 
         :return:
         """
-        if self.signature is None and len(self.signatures) == 0:
+        if not self.signatures:
             raise MalformedDocumentError("No signature, can not create raw format")
-        raw = self.raw()
 
-        if self.signature is not None:
-            signed_raw = raw + self.signature + "\n"
-        else:
-            signed_raw = raw
-            for signature in self.signatures:
-                signed_raw += "{0}\n".format(signature)
+        signed_raw = self.raw()
+        for signature in self.signatures:
+            signed_raw += f"{signature}\n"
 
         return signed_raw
 
-    def multi_sign(self, keys: List[SigningKey]) -> None:
+    def sign(self, key: SigningKey) -> None:
+        raise Exception("sign() is not implemented, use multi_sign()")
+
+    def multi_sign(self, keys: Union[SigningKey, List[SigningKey]]) -> None:
         """
         Sign the current document with multiple keys
 
-        Warning : current signatures will be replaced with the new ones.
-
-        :param keys: List of libnacl keys instance
+        :param keys: Libnacl key or list of them
         """
-        self.signatures = list()
+        if isinstance(keys, SigningKey):
+            keys = [keys]
+
         for key in keys:
             signature = base64.b64encode(key.signature(bytes(self.raw(), "ascii")))
             logging.debug("Signature : \n%s", signature.decode("ascii"))
             self.signatures.append(signature.decode("ascii"))
 
-    def check_signatures(self, pubkeys: List[str]):
+    def check_signature(self, pubkey: str):
+        raise Exception("check_signature() is not implemented, use check_signatures()")
+
+    def check_signatures(self, pubkeys: Union[str, List[str]]):
         """
         Check if the signatures matches the pubkeys
 
-        :param pubkeys: List of Base58 public keys
+        :param pubkeys: Base58 public key or list of them
 
         :return:
         """
-        if len(self.signatures) == 0:
+        if not self.signatures:
             raise Exception("No signatures, can not check signatures")
 
+        if isinstance(pubkeys, str):
+            pubkeys = [pubkeys]
+
         if len(self.signatures) != len(pubkeys):
             raise Exception("Number of pubkeys not equal to number of signatures")
 
@@ -998,7 +991,7 @@ class SimpleTransaction(Transaction):
         outputs: List[OutputSource],
         comment: str,
         time: int = 0,
-        signing_key: SigningKey = None,
+        signing_keys: Optional[Union[SigningKey, List[SigningKey]]] = None,
         version: int = VERSION,
         currency: str = G1_CURRENCY_CODENAME,
     ) -> None:
@@ -1013,7 +1006,7 @@ class SimpleTransaction(Transaction):
         :param outputs: List of OutputSource instances
         :param comment: Comment field
         :param time: time when the transaction enters the blockchain (default=0)
-        :param signing_key: SigningKey instance to sign the document (default=None)
+        :param signing_keys: SigningKey instance to sign the document (default=None)
         :param version: Document version (default=transaction.VERSION)
         :param currency: Currency codename (default=constants.CURRENCY_CODENAME_G1)
         """
@@ -1026,7 +1019,7 @@ class SimpleTransaction(Transaction):
             outputs,
             comment,
             time=time,
-            signing_key=signing_key,
+            signing_keys=signing_keys,
             version=version,
             currency=currency,
         )
diff --git a/examples/send_transaction.py b/examples/send_transaction.py
index 7560facd..1ddf631b 100644
--- a/examples/send_transaction.py
+++ b/examples/send_transaction.py
@@ -15,6 +15,7 @@
 
 import getpass
 import urllib
+from typing import List, Union
 
 from duniterpy.api import bma
 from duniterpy.api.client import Client
@@ -42,7 +43,7 @@ def get_transaction_document(
     source: dict,
     from_pubkey: str,
     to_pubkey: str,
-    signing_key: SigningKey,
+    signing_keys: Union[SigningKey, List[SigningKey]],
 ) -> Transaction:
     """
     Return a Transaction document
@@ -96,7 +97,7 @@ def get_transaction_document(
         unlocks=unlocks,
         outputs=outputs,
         comment="",
-        signing_key=signing_key,
+        signing_keys=signing_keys,
         currency=current_block["currency"],
     )
 
diff --git a/tests/key/test_verifying_key.py b/tests/key/test_verifying_key.py
index c6ffb7ae..38016937 100644
--- a/tests/key/test_verifying_key.py
+++ b/tests/key/test_verifying_key.py
@@ -147,4 +147,4 @@ Solde huile Millepertuis
 rgjOmzFH5h+hkDbJLk1b88X7Z83HMgTa5rBckeMSdF/yZtItN3zMn09MphcXjffdrKcK+MebwoisLJqV+jXrDg==
 """
         tx = Transaction.from_compact(transaction_document, "g1")
-        self.assertTrue(tx.check_signature(tx.issuers[0]))
+        self.assertTrue(tx.check_signatures(tx.issuers))
-- 
GitLab