├── .gitignore ├── LICENSE ├── README.md ├── functional_test.py ├── musig2.py └── unit_tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Files created by this project 132 | secret.key 133 | secret_nonces 134 | public_nonces 135 | public_keys 136 | s_values 137 | message 138 | musig2-test 139 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Samuel Dobson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # musig2-py 2 | Experimental MuSig2 python code, not for production use! This is just for testing things out. As experimental code, please also expect it to change in breaking ways between commits. 3 | 4 | MuSig2 is described in [this paper](https://eprint.iacr.org/2020/1261) by Jonas Nick, Tim Ruffing, and Yannick Seurin, which was published at CRYPTO'21. This implementation also (maybe?) follows the draft specification [here](https://github.com/ElementsProject/secp256k1-zkp/pull/157), but compatibility with [zecp256k1-zkp](https://github.com/ElementsProject/secp256k1-zkp)'s implementation is untested. 5 | 6 | Public keys are encoded as 32 bytes, assuming an even y coordinate, as in [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki). 7 | 8 | Nonces consist of two 33-bytes public keys concatenated, for 66 bytes in total. These 33-byte public keys are in compressed form, and consist of a parity byte (`0x02` if the y-coordinate is even and `0x03` otherwise), followed by the 32-byte x-coordinate. 9 | 10 | Signatures are 64 bytes. The first 32 bytes encode the x-coordinate of the point R (which is again assumed to have an even y coordinate). The second 32 bytes encode the integer s. This makes them compatible with [BIP-340](https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki), and hence valid as [BIP-341](https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki) Taproot Schnorr signatures. 11 | 12 | ## Usage 13 | 14 | 1. First generate a public and private keypair: 15 | 16 | ``` 17 | > python3 musig2.py keygen 18 | Your public key: 19 | 1a9abf430360780ce7c9fdcb63381d0fe1dcb8618b93ee5076fe8cd9bc3eece1 20 | ``` 21 | 22 | This will create a file `secret.key` containing the secret key for the above public key. Keep this safe. 23 | 24 | 2. Send your public key to all other participants involved with this MuSig2 aggregate signing key. 25 | 26 | 3. Receive from all participants their public keys and create a file called `public_keys` containing all these keys (including your own). The order is not important. For example: 27 | 28 | ``` 29 | 1a9abf430360780ce7c9fdcb63381d0fe1dcb8618b93ee5076fe8cd9bc3eece1 30 | 4c778668e7cb6467a04c190eb9dad466006f84e18f35598f6ac5a4662009102d 31 | 8470d74a5ff04928eaec2e1dc5562c1a7ea7a7cce913901bdec031bda84eeecf 32 | ``` 33 | 34 | 4. Generate the aggregate public key: 35 | 36 | ``` 37 | > python3 musig2.py aggregatekeys 38 | Aggregate public key: 39 | 6a3ebe79463836eeff69fffe493d3c42d8c5bbd47fcfaf40aa6a6026c45ab535 40 | ``` 41 | 42 | This public key will be the final public key used for verification of the signature. It can be used as many times as you (and your co-signers) like. You will need to generate new nonces every time you wish to sign with it, however. 43 | 44 | 5. Generate a single-use nonce: 45 | 46 | ``` 47 | > python3 musig2.py noncegen 48 | WARNING: Only use this nonce once, then generate a new one. 49 | Reusing nonces to sign different messages will leak your secret key. 50 | Your new nonce: 51 | 02cf08c1684b35870f2a9231d75a1e8b9ac343f31e0622f7dac55beb195607cd170238cbaebcac547896d97bb7d2ca6af9b404e01c85652e5c1356161e13d651431b 52 | ``` 53 | 54 | This will also create a file `secret_nonces` containing the secrets corresponding to this nonce. 55 | 56 | 6. Send your nonce to all other participants in the multisig, in preparation to sign a message. 57 | 58 | 7. Receive from all participants their nonces for this signing session, and create a file called `public_nonces` containing all these nonces. The order of the participants is not important. For example: 59 | 60 | ``` 61 | 02cf08c1684b35870f2a9231d75a1e8b9ac343f31e0622f7dac55beb195607cd170238cbaebcac547896d97bb7d2ca6af9b404e01c85652e5c1356161e13d651431b 62 | 02949a99f1a2fa58339981fa437fc763b622b0f9373bf5c60d788474ceced9cfb20220fd11a9d5fcb4a7a4b99c211b3b7e8e59ab9a764d3a0727f93b15d70482d7c8 63 | 023e2eae90e5bc2bc53cca532d438210a8908b5fb1f01977befab21f59be173e6602a209a7e4272b4606d6517338322f03a2a17b1f77d10f1ded8fec7d56dd337b1a 64 | ``` 65 | 66 | 6. Create a file called `message` containing the message you wish to sign. The contents of the file are interpreted as bytes, not as a string. You can alternatively specify a filename. Then use the `sign ` command to generate a partial signature. 67 | 68 | ``` 69 | > cat message 70 | hello world 71 | > python3 musig2.py sign 72 | Aggregate key: 73 | 6a3ebe79463836eeff69fffe493d3c42d8c5bbd47fcfaf40aa6a6026c45ab535 74 | Signature R: 75 | f65afa33eecff5bd837bd218075f4d4074c03eadd65e78dbd3cc66e2f55f10cd 76 | Partial signature s_1: 77 | fbf8fa92eda16cbac787187d8d38430e1234b1f08d8a5304a063e02bb3140808 78 | ``` 79 | 80 | This will delete the secret nonces previous generated to ensure they are not reused. The aggregate key, `R` value, and your partial signature `s_1` will be written to `message.partsig` (or correspondingly for the filename specified) though, in case you forget to copy it from the command line output. 81 | 82 | 7. Send the partial signature `s_1` to all other parties and receive their partial signatures. Create a file called `s_values` containing all these partial signatures, including your own (order does not matter): 83 | 84 | ``` 85 | fbf8fa92eda16cbac787187d8d38430e1234b1f08d8a5304a063e02bb3140808 86 | 786fab4e32625eec618ab64c4bb9c080b02d57df867bd4364a7739dd8446eb31 87 | 423773284754d75f2f807e57b6a50648f481dc380908b31f5e92c5c7d6996ebf 88 | ``` 89 | 90 | 8. Aggregate the partial signatures: 91 | 92 | ``` 93 | > python3 musig2.py aggregatesignature 94 | Hex-encoded signature: f65afa33eecff5bd837bd218075f4d4074c03eadd65e78dbd3cc66e2f55f10cdb6a019096758a30658924d218f9709d8fc3509216dc63a1e899b81443dbe20b7 95 | ``` 96 | 97 | Again, you can optionally specify the filename of the message being signed if you did not use the default `message`. 98 | 99 | 9. Verify the signature created: 100 | 101 | ``` 102 | > python3 musig2.py verify 6a3ebe79463836eeff69fffe493d3c42d8c5bbd47fcfaf40aa6a6026c45ab535 f65afa33eecff5bd837bd218075f4d4074c03eadd65e78dbd3cc66e2f55f10cdb6a019096758a30658924d218f9709d8fc3509216dc63a1e899b81443dbe20b7 103 | Signature is valid: True 104 | ``` 105 | 106 | The format for the verification command is 107 | `verify python3 unit_tests.py 114 | test_seckey_gen PASSED 115 | test_read_write_bytes PASSED 116 | test_point_serialisation PASSED 117 | test_aggregate_public_keys PASSED 118 | test_aggregate_nonces PASSED 119 | test_compute_R PASSED 120 | test_compute_s PASSED 121 | ``` 122 | 123 | The functional tests run the code externally simulating multiple users in a key establishment and signing session. 124 | ``` 125 | > python3 functional_test.py 126 | X: ac4a3b78a1368de26f96346cdf87149a2e2d6201b14559120f73c78b1b8253c3 127 | S: 3d18300bbcac308f7f860cc263fe0cafd8a54c0b0a18c953b3f5884dd5012e03bcc45d03cab195223bc6bf98f85f7a4ac33a29eb1d46faac172aec9649cfa678 128 | Signature is valid: True 129 | ``` 130 | -------------------------------------------------------------------------------- /functional_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import subprocess 4 | 5 | import musig2 6 | 7 | children = ['one', 'two', 'three'] 8 | 9 | sig = '' 10 | X = b'' 11 | 12 | def create_dirs(): 13 | if os.path.exists("musig2-test"): 14 | shutil.rmtree("musig2-test") 15 | os.mkdir("musig2-test") 16 | for child in children: 17 | os.mkdir(f"musig2-test/{child}") 18 | musig2.write_bytes("hello world\n".encode(), f"musig2-test/{child}/message") 19 | 20 | def gen_pub_keys(): 21 | keys = b'' 22 | for child in children: 23 | one = subprocess.Popen(["python3", "../../musig2.py", "keygen"], 24 | cwd=f"musig2-test/{child}", 25 | stdout=subprocess.PIPE 26 | ) 27 | stdout, _ = one.communicate() 28 | pubkey = stdout.strip().split(b'\n')[-1] 29 | keys += pubkey + b'\n' 30 | for child in children: 31 | musig2.write_bytes(keys, f"musig2-test/{child}/public_keys") 32 | 33 | def gen_nonces(): 34 | nonces = b'' 35 | for child in children: 36 | one = subprocess.Popen(["python3", "../../musig2.py", "noncegen"], 37 | cwd=f"musig2-test/{child}", 38 | stdout=subprocess.PIPE 39 | ) 40 | stdout, _ = one.communicate() 41 | stdout = stdout.strip().split(b'\n') 42 | nonces += stdout[-1] + b'\n' 43 | for child in children: 44 | musig2.write_bytes(nonces, f"musig2-test/{child}/public_nonces") 45 | 46 | def do_sign(): 47 | s_values = b'' 48 | for child in children: 49 | one = subprocess.Popen(["python3", "../../musig2.py", "sign"], 50 | cwd=f"musig2-test/{child}", 51 | stdout=subprocess.PIPE 52 | ) 53 | stdout, _ = one.communicate() 54 | stdout = stdout.strip().split(b'\n') 55 | s_value = stdout[-1] 56 | global X 57 | X = stdout[-5].decode() 58 | s_values += s_value + b'\n' 59 | for child in children: 60 | musig2.write_bytes(s_values, f"musig2-test/{child}/s_values") 61 | 62 | def aggregate_signatures(): 63 | one = subprocess.Popen(["python3", "../../musig2.py", "aggregatesignature"], 64 | cwd=f"musig2-test/one", 65 | stdout=subprocess.PIPE 66 | ) 67 | stdout, _ = one.communicate() 68 | global sig 69 | sig = stdout.strip().split(b'\n')[-1].split(b' ')[-1].decode() 70 | 71 | def do_verify(): 72 | print(f"X: {X}") 73 | #print(f"R: {R}") 74 | print(f"S: {sig}") 75 | one = subprocess.Popen(["python3", "../../musig2.py", "verify", X, sig], 76 | cwd=f"musig2-test/one", 77 | stdout=subprocess.PIPE 78 | ) 79 | stdout, _ = one.communicate() 80 | print(stdout.decode()) 81 | 82 | def remove_single_use_files(): 83 | for child in children: 84 | if os.path.exists(f"musig2-test/{child}/s_values"): 85 | os.remove(f"musig2-test/{child}/s_values") 86 | if os.path.exists(f"musig2-test/{child}/public_nonces"): 87 | os.remove(f"musig2-test/{child}/public_nonces") 88 | 89 | def cleanup(): 90 | if os.path.exists("musig2-test"): 91 | shutil.rmtree("musig2-test") 92 | 93 | def main(): 94 | 95 | create_dirs() 96 | gen_pub_keys() 97 | gen_nonces() 98 | do_sign() 99 | aggregate_signatures() 100 | do_verify() 101 | 102 | remove_single_use_files() 103 | 104 | # Sign a second message with the same public keys 105 | gen_nonces() 106 | do_sign() 107 | aggregate_signatures() 108 | do_verify() 109 | 110 | cleanup() 111 | 112 | 113 | if __name__ == "__main__": 114 | main() 115 | -------------------------------------------------------------------------------- /musig2.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import os 3 | import secrets 4 | import sys 5 | from typing import List, Optional, Tuple 6 | 7 | # secp256k1 finite field order (p) and group order (n) 8 | p = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F 9 | n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 10 | 11 | # Points are tuples of X and Y coordinates and the point at infinity is 12 | # represented by the None keyword. 13 | G = (0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798, 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8) 14 | 15 | # Number of nonces used by each signer. 2 is proven secure for AGM + AOMDL 16 | # 4 is proven secure for just AOMDL assumption. 17 | nu = 2 18 | 19 | ########## POINT FUNCTIONS ########## 20 | 21 | Point = Tuple[int, int] 22 | 23 | def is_infinite(P: Optional[Point]) -> bool: 24 | return P is None 25 | 26 | def x(P: Point) -> int: 27 | assert not is_infinite(P) 28 | return P[0] 29 | 30 | def y(P: Point) -> int: 31 | assert not is_infinite(P) 32 | return P[1] 33 | 34 | def has_even_y(P: Point) -> bool: 35 | return y(P) & 1 == 0 36 | 37 | def point_add(P1: Optional[Point], P2: Optional[Point]) -> Optional[Point]: 38 | if P1 is None: 39 | return P2 40 | if P2 is None: 41 | return P1 42 | if (x(P1) == x(P2)) and (y(P1) != y(P2)): 43 | return None 44 | if P1 == P2: 45 | lam = (3 * x(P1) * x(P1) * pow(2 * y(P1), p - 2, p)) % p 46 | else: 47 | lam = ((y(P2) - y(P1)) * pow(x(P2) - x(P1), p - 2, p)) % p 48 | x3 = (lam * lam - x(P1) - x(P2)) % p 49 | return (x3, (lam * (x(P1) - x3) - y(P1)) % p) 50 | 51 | def point_mul(P: Optional[Point], n: int) -> Optional[Point]: 52 | R = None 53 | for i in range(256): 54 | if (n >> i) & 1: 55 | R = point_add(R, P) 56 | P = point_add(P, P) 57 | return R 58 | 59 | def int_from_bytes(b: bytes) -> int: 60 | return int.from_bytes(b, byteorder="big") 61 | 62 | def bytes_from_int(x: int) -> bytes: 63 | return x.to_bytes(32, byteorder="big") 64 | 65 | def bytes_from_point(P: Point, compressed: bool = False) -> bytes: 66 | x_coord = bytes_from_int(x(P)) 67 | if compressed: 68 | if has_even_y(P): 69 | return b'\x02' + x_coord 70 | else: 71 | return b'\x03' + x_coord 72 | return x_coord 73 | 74 | def lift_x(b: bytes) -> Optional[Point]: 75 | if len(b) == 32: 76 | x = int_from_bytes(b) 77 | even = True 78 | else: 79 | x = int_from_bytes(b[1:]) 80 | even = (b[0] == 2) 81 | if x >= p: 82 | return None 83 | y_sq = (pow(x, 3, p) + 7) % p 84 | y = pow(y_sq, (p + 1) // 4, p) 85 | if pow(y, 2, p) != y_sq: 86 | return None 87 | if (even and y & 1 != 0) or ((not even) and y & 1 == 0): 88 | y = p - y 89 | return (x, y) 90 | 91 | def pubkey_gen(seckey: bytes, compressed: bool = False) -> bytes: 92 | d0 = int_from_bytes(seckey) 93 | if not (1 <= d0 <= n - 1): 94 | raise ValueError('The secret key must be an integer in the range 1..n-1.') 95 | P = point_mul(G, d0) 96 | assert P is not None 97 | return bytes_from_point(P, compressed) 98 | 99 | def seckey_gen(force_even_y: bool = True) -> bytes: 100 | # choose random integer below the order of the curve 101 | seckey_int = secrets.randbelow(n) 102 | # Check that this int gives a public key with even y 103 | P = point_mul(G, seckey_int) 104 | if force_even_y and not has_even_y(P): 105 | seckey_int = n - seckey_int 106 | # Convert it to bytes 107 | seckey_bytes = bytes_from_int(seckey_int) 108 | # Return the secret key 109 | return seckey_bytes 110 | 111 | ########## FILE FUNCTIONS ########## 112 | 113 | SECRET_KEY_FILE = 'secret.key' 114 | PUBLIC_KEY_LIST_FILE = 'public_keys' 115 | SECRET_NONCE_FILE = 'secret_nonces' 116 | PUBLIC_NONCE_LIST_FILE = 'public_nonces' 117 | DEFAULT_MESSAGE_FILE = 'message' 118 | S_VALUES_FILE = 's_values' 119 | 120 | def write_bytes(bytes_to_write: bytes, filename: str) -> bool: 121 | if os.path.isfile(filename): 122 | print(f"File {filename} already exists, will not overwrite.") 123 | return False 124 | with open(filename, 'wb') as f: 125 | return f.write(bytes_to_write) > 0 126 | 127 | def read_bytes(filename: str) -> bytes: 128 | if not os.path.isfile(filename): 129 | print(f"Error: file {filename} does not exist.") 130 | quit() 131 | with open(filename, 'rb') as f: 132 | read_bytes = f.read() 133 | if len(read_bytes) <= 0: 134 | print(f"Error: file {filename} is empty.") 135 | quit() 136 | return read_bytes 137 | 138 | def write_bytes_list_to_hex(bytes_list: List[bytes], filename: str) -> bool: 139 | # Opening with 'w' will overwrite the file if it exists 140 | with open(filename, 'w') as f: 141 | for byte_string in bytes_list: 142 | if not f.write(f"{byte_string.hex()}\n") > 0: 143 | return False 144 | return True 145 | 146 | def read_bytes_from_hex_list(filename: str) -> List[bytes]: 147 | if not os.path.isfile(filename): 148 | print(f"Error: file {filename} does not exist.") 149 | quit() 150 | hex_list = [] 151 | with open(filename, 'r') as f: 152 | for line in f: 153 | hex_bytes = bytes.fromhex(line) 154 | hex_list.append(hex_bytes) 155 | if not hex_list: 156 | print(f"Error: file {filename} is empty.") 157 | quit() 158 | return hex_list 159 | 160 | def get_message(filename: str) -> bytes: 161 | message = read_bytes(filename) 162 | if not message: 163 | quit() 164 | return message 165 | 166 | ########## HELPER FUNCTIONS ########## 167 | 168 | # This uses BIP-340's tagged hash, SHA256(SHA256(tag) || SHA256(tag) || x) 169 | def tagged_hash(tag: str, msg: bytes) -> bytes: 170 | tag_hash = hashlib.sha256(tag.encode()).digest() 171 | return hashlib.sha256(tag_hash + tag_hash + msg).digest() 172 | 173 | # Returns true if public_key is the second unique key in key_list 174 | def is_second_unique_key(key_list, public_key): 175 | for key in key_list: 176 | if key != key_list[0]: 177 | if key == public_key: 178 | return True 179 | else: 180 | return False 181 | return False 182 | 183 | # Takes a list of public keys, and another key, and creates the aggregation coefficient for that key 184 | def key_agg_coeff(key_set: List[bytes], public_key: bytes) -> int: 185 | # Sort the set of keys in lexicographical order 186 | sorted_keys = sorted(key_set) 187 | # If this is the second unique key in the list, we optimise by using coefficient 1 188 | if is_second_unique_key(sorted_keys, public_key): 189 | return 1 190 | # Compute the hash of the sorted key list 191 | L = tagged_hash("KeyAgg list", b''.join(sorted_keys)) 192 | hash_bytes = tagged_hash("KeyAgg coefficient", L + public_key) 193 | # Convert the coefficient to an integer modulo the curve order 194 | coefficient = int_from_bytes(hash_bytes) % n 195 | return coefficient 196 | 197 | ########## MUSIG2 FUNCTIONS ########## 198 | 199 | def aggregate_public_keys(public_key_list: List[bytes], own_key: Optional[bytes]) -> Tuple[Point, int]: 200 | aggregate_key = None 201 | own_coeff = 0 202 | for key_bytes in public_key_list: 203 | # a_i is an integer coefficient 204 | a_i = key_agg_coeff(public_key_list, key_bytes) 205 | # If this key is the one specified, save the coefficient to return 206 | # This also ensures the key specified is actually part of the list 207 | if own_key == key_bytes: 208 | own_coeff = a_i 209 | # All the public keys should have implicitly even y coordinates 210 | pubkey_i = lift_x(key_bytes) 211 | if not pubkey_i: 212 | print(f"Error: Public key {key_bytes.hex()} is invalid.") 213 | quit() 214 | # Multiply the key by its coefficient 215 | a_i_pk = point_mul(pubkey_i, a_i) 216 | # Add the resulting point to our sum 217 | aggregate_key = point_add(aggregate_key, a_i_pk) 218 | assert not is_infinite(aggregate_key) 219 | if own_key is not None: 220 | assert own_coeff > 0 221 | return aggregate_key, own_coeff 222 | 223 | def aggregate_nonces(nonces_to_aggregate: List[bytes]) -> List[Point]: 224 | # Every nu nonces are a set corresponding to one signer 225 | aggregated_nonces = [] 226 | for j in range(nu): 227 | R_j = None 228 | for combined_nonce in nonces_to_aggregate: 229 | nonce_component = combined_nonce[33*j : 33*(j + 1)] 230 | point = lift_x(nonce_component) 231 | R_j = point_add(R_j, point) 232 | if is_infinite(R_j): 233 | # From spec: there is at least one dishonest signer (except with negligible probability). 234 | # Continue with arbitrary use of point G so the dishonest signer can be caught later 235 | R_j = G 236 | aggregated_nonces.append(R_j) 237 | return aggregated_nonces 238 | 239 | def hash_nonces(agg_pubkey: bytes, nonces: List[bytes], msg: bytes) -> int: 240 | bytes_to_hash = b''.join(nonces) + agg_pubkey + msg 241 | hash_bytes = tagged_hash("MuSig/noncecoef", bytes_to_hash) 242 | b = int_from_bytes(hash_bytes) % n 243 | return b 244 | 245 | def chall_hash(agg_pubkey: bytes, R: bytes, msg: bytes) -> int: 246 | bytes_to_hash = b'' + R + agg_pubkey + msg 247 | # Use the BIP-340 challenge hash so the final signature is a valid BIP-340 schnorr signature 248 | hash_bytes = tagged_hash("BIP0340/challenge", bytes_to_hash) 249 | return int_from_bytes(hash_bytes) 250 | 251 | def compute_R(nonces: List[Point], b: int) -> Point: 252 | R = None 253 | for j in range(nu): 254 | R_j = point_mul(nonces[j], (b**j) % n) 255 | R = point_add(R, R_j) 256 | assert not is_infinite(R) 257 | return R 258 | 259 | def compute_s(chall: int, secret: bytes, coeff: int, nonce_secrets: List[bytes], b: int) -> int: 260 | # s = c*a_1*x_1 + \sum{ r_1,j * b^{j-1} } 261 | s = (chall * coeff * int_from_bytes(secret)) % n 262 | for j in range(nu): 263 | r_1j = int_from_bytes(nonce_secrets[j]) 264 | b_coeff = (b**j) % n 265 | s += (r_1j * b_coeff) 266 | s %= n 267 | return s 268 | 269 | def verify_sig(aggregate_key_bytes: bytes, msg: bytes, R_bytes: bytes, s: int) -> bool: 270 | left = point_mul(G, s) 271 | R = lift_x(R_bytes) 272 | aggregate_key = lift_x(aggregate_key_bytes) 273 | c = chall_hash(aggregate_key_bytes, R_bytes, msg) 274 | right = point_add(R, point_mul(aggregate_key, c)) 275 | return left == right 276 | 277 | 278 | def main(): 279 | if len(sys.argv) < 2: 280 | print("Available commands: keygen, noncegen, aggregatekeys, sign, aggregatesignature, verify") 281 | quit() 282 | 283 | command = sys.argv[1] 284 | 285 | # Generate a public + private keypair 286 | if command == "keygen": 287 | seckey = seckey_gen() 288 | if not write_bytes(seckey, SECRET_KEY_FILE): 289 | seckey = read_bytes(SECRET_KEY_FILE) 290 | pubkey = pubkey_gen(seckey) 291 | print(f"Your public key:\n{pubkey.hex()}") 292 | quit() 293 | 294 | # Generate some random nonces 295 | elif command == "noncegen": 296 | nonce_secrets = [] 297 | nonces = b'' 298 | print("WARNING: Only use this nonce once, then generate a new one.") 299 | print("Reusing nonces to sign different messages will leak your secret key.") 300 | for _ in range(nu): 301 | # Generate a secret key 302 | r_1j = seckey_gen(force_even_y = False) 303 | # R_1j will be in 33-byte compressed key form with a parity byte 304 | R_1j = pubkey_gen(r_1j, compressed=True) 305 | # Add this newly generated keypair to the lists 306 | nonce_secrets.append(r_1j) 307 | nonces += R_1j 308 | # Print the public nonce 309 | print(f"Your new nonce:\n{nonces.hex()}") 310 | # Encode the nonce secrets as a newline-separated list 311 | write_bytes_list_to_hex(nonce_secrets, SECRET_NONCE_FILE) 312 | quit() 313 | 314 | # Compute the aggregate public key 315 | elif command == "aggregatekeys": 316 | public_keys_list = read_bytes_from_hex_list(PUBLIC_KEY_LIST_FILE) 317 | combined_key, _ = aggregate_public_keys(public_keys_list, None) 318 | combined_key_bytes = bytes_from_point(combined_key) 319 | print(f"Aggregate public key:\n{combined_key_bytes.hex()}") 320 | quit() 321 | 322 | # Generate a partial signature from our secret key w.r.t. the aggregated key and nonces 323 | elif command == "sign": 324 | if len(sys.argv) > 3: 325 | print("Usage: sign [message_filename (optional)]") 326 | quit() 327 | elif len(sys.argv) == 3: 328 | message_file = sys.argv[2] 329 | else: 330 | message_file = DEFAULT_MESSAGE_FILE 331 | message = get_message(message_file) 332 | seckey = read_bytes(SECRET_KEY_FILE) 333 | pubkey = pubkey_gen(seckey) 334 | 335 | # Compute the aggregate public key 336 | public_keys_list = read_bytes_from_hex_list(PUBLIC_KEY_LIST_FILE) 337 | combined_key, a_1 = aggregate_public_keys(public_keys_list, pubkey) 338 | combined_key_bytes = bytes_from_point(combined_key) 339 | print(f"Aggregate key:\n{combined_key_bytes.hex()}") 340 | 341 | # Aggregate the nonces from all participants and compute R 342 | public_nonce_list = read_bytes_from_hex_list(PUBLIC_NONCE_LIST_FILE) 343 | if len(public_nonce_list) != len(public_keys_list): 344 | print("Error: mismatch between number of nonces and number of public keys.") 345 | quit() 346 | aggregated_nonce_points = aggregate_nonces(public_nonce_list) 347 | aggregated_nonce_bytes = [bytes_from_point(R, compressed=True) for R in aggregated_nonce_points] 348 | b = hash_nonces(combined_key_bytes, aggregated_nonce_bytes, message) 349 | R = compute_R(aggregated_nonce_points, b) 350 | R_bytes = bytes_from_point(R) 351 | print(f"Signature R:\n{R_bytes.hex()}") 352 | 353 | # Compute challenge 354 | c = chall_hash(combined_key_bytes, R_bytes, message) 355 | 356 | # Sign 357 | nonce_secrets = read_bytes_from_hex_list(SECRET_NONCE_FILE) 358 | if not has_even_y(R): 359 | # Negate all the nonce secrets if the R value has an odd y coordinate 360 | nonce_secrets = [bytes_from_int(n - int_from_bytes(r)) for r in nonce_secrets] 361 | if not has_even_y(combined_key): 362 | seckey = bytes_from_int(n - int_from_bytes(seckey)) 363 | s_1 = compute_s(c, seckey, a_1, nonce_secrets, b) 364 | s_1_bytes = bytes_from_int(s_1) 365 | print(f"Partial signature s_1:\n{s_1_bytes.hex()}") 366 | 367 | with open(f"{message_file}.partsig", "w") as f: 368 | f.write(f"{combined_key_bytes.hex()}\n{R_bytes.hex()}\n{s_1_bytes.hex()}\n") 369 | 370 | # Delete the nonce secrets to ensure they are not reused multiple times 371 | os.remove(SECRET_NONCE_FILE) 372 | quit() 373 | 374 | # Take a list of partial signatures and combine them into a valid signature under the aggregate public key 375 | elif command == "aggregatesignature": 376 | if len(sys.argv) > 3: 377 | print("Usage: aggregatesignature [message_filename (optional)]") 378 | quit() 379 | elif len(sys.argv) == 3: 380 | message_file = sys.argv[2] 381 | else: 382 | message_file = DEFAULT_MESSAGE_FILE 383 | message = get_message(message_file) 384 | 385 | # Sum the partial signature values from all signers 386 | s = 0 387 | sig_bytes_list = read_bytes_from_hex_list(S_VALUES_FILE) 388 | for s_i in sig_bytes_list: 389 | s += int_from_bytes(s_i) 390 | s %= n 391 | s_bytes = bytes_from_int(s) 392 | 393 | # Retrieve the R value from the partsig file 394 | partsig_bytes_list = read_bytes_from_hex_list(f"{message_file}.partsig") 395 | R_bytes = partsig_bytes_list[1] 396 | # Combine to produce the final signature 397 | signature_bytes = R_bytes + s_bytes 398 | print(f"Hex-encoded signature:\n{signature_bytes.hex()}") 399 | quit() 400 | 401 | elif command == "verify": 402 | if len(sys.argv) < 4 or len(sys.argv) > 5: 403 | print("Usage: verify [pubkey] [signature] [message_filename (optional)]") 404 | quit() 405 | 406 | pubkey = bytes.fromhex(sys.argv[2]) 407 | if len(pubkey) != 32: 408 | print("Error: length of public key must be 32 bytes") 409 | quit() 410 | 411 | signature_bytes = bytes.fromhex(sys.argv[3]) 412 | if len(signature_bytes) != 64: 413 | print("Error: length of signature must be 64 bytes") 414 | quit() 415 | 416 | if len(sys.argv) == 5: 417 | message_file = sys.argv[4] 418 | else: 419 | message_file = DEFAULT_MESSAGE_FILE 420 | message = get_message(message_file) 421 | 422 | R = signature_bytes[0:32] 423 | s = int_from_bytes(signature_bytes[32:64]) 424 | 425 | valid = verify_sig(pubkey, message, R, s) 426 | print(f"Signature is valid: {valid}") 427 | 428 | else: 429 | print("Unknown command.") 430 | quit() 431 | 432 | if __name__ == "__main__": 433 | main() 434 | -------------------------------------------------------------------------------- /unit_tests.py: -------------------------------------------------------------------------------- 1 | import musig2 as m2 2 | 3 | import random 4 | import os 5 | import sys 6 | 7 | def test_seckey_gen(): 8 | for _ in range(10): 9 | key = m2.seckey_gen() 10 | pubkey = m2.pubkey_gen(key) 11 | assert pubkey is not None 12 | key_int = m2.int_from_bytes(key) 13 | pubkey_check = m2.point_mul(m2.G, key_int) 14 | assert m2.has_even_y(pubkey_check) 15 | assert m2.lift_x(m2.bytes_from_point(pubkey_check)) == pubkey_check 16 | assert m2.bytes_from_point(pubkey_check) == pubkey 17 | sys.stdout.write('.') 18 | sys.stdout.flush() 19 | sys.stdout.write('\rtest_seckey_gen PASSED\n') 20 | sys.stdout.flush() 21 | 22 | def test_read_write_bytes(): 23 | for _ in range(10): 24 | bytes = random.randbytes(32) 25 | m2.write_bytes(bytes, 'test_read_write') 26 | read_bytes = m2.read_bytes('test_read_write') 27 | assert bytes == read_bytes 28 | os.remove('test_read_write') 29 | 30 | bytes_list = [random.randbytes(32) for _ in range(10)] 31 | assert m2.write_bytes_list_to_hex(bytes_list, 'test_read_hex_list') 32 | read_bytes_list = m2.read_bytes_from_hex_list('test_read_hex_list') 33 | assert read_bytes_list == bytes_list 34 | os.remove('test_read_hex_list') 35 | sys.stdout.write('.') 36 | sys.stdout.flush() 37 | sys.stdout.write('\rtest_read_write_bytes PASSED\n') 38 | sys.stdout.flush() 39 | 40 | def test_point_serialisation(): 41 | for _ in range(10): 42 | seckey = m2.seckey_gen() 43 | pubkey = m2.pubkey_gen(seckey) 44 | pubkey_point = m2.lift_x(pubkey) 45 | xonly_pubkey = m2.bytes_from_point(pubkey_point) 46 | assert xonly_pubkey == pubkey 47 | 48 | seckey = m2.seckey_gen(force_even_y=False) 49 | pubkey = m2.pubkey_gen(seckey, compressed=True) 50 | pubkey_point = m2.lift_x(pubkey) 51 | pubkey_check = m2.point_mul(m2.G, m2.int_from_bytes(seckey)) 52 | assert pubkey_check == pubkey_point 53 | compressed_pubkey = m2.bytes_from_point(pubkey_point, compressed=True) 54 | assert compressed_pubkey == pubkey 55 | xonly_pubkey = m2.bytes_from_point(pubkey_point) 56 | assert xonly_pubkey == compressed_pubkey[1:] 57 | 58 | sys.stdout.write('.') 59 | sys.stdout.flush() 60 | sys.stdout.write('\rtest_point_serialisation PASSED\n') 61 | sys.stdout.flush() 62 | 63 | def test_aggregate_public_keys(): 64 | for _ in range(5): 65 | secrets = [] 66 | pubkeys = [] 67 | coeffs = [] 68 | for _ in range(5): 69 | sec_i = m2.seckey_gen() 70 | secrets.append(m2.int_from_bytes(sec_i)) 71 | pub_i = m2.pubkey_gen(sec_i) 72 | pubkeys.append(pub_i) 73 | with open('test_aggregate_public_keys', 'w') as f: 74 | for k in pubkeys: 75 | f.write(k.hex() + '\n') 76 | public_keys_list = m2.read_bytes_from_hex_list('test_aggregate_public_keys') 77 | combined_key = None 78 | for k in pubkeys: 79 | ck, coeff_i = m2.aggregate_public_keys(public_keys_list, k) 80 | if combined_key is None: 81 | combined_key = ck 82 | else: 83 | assert combined_key == ck 84 | coeffs.append(coeff_i) 85 | assert not m2.is_infinite(combined_key) 86 | combined_sec = 0 87 | for sec, coeff in zip(secrets, coeffs): 88 | if m2.has_even_y(combined_key): 89 | sec = m2.n - sec 90 | combined_sec += sec * coeff 91 | combined_sec %= m2.n 92 | pubkey_check = m2.point_mul(m2.G, combined_sec) 93 | assert m2.bytes_from_point(pubkey_check) == m2.bytes_from_point(combined_key) 94 | os.remove('test_aggregate_public_keys') 95 | sys.stdout.write('.') 96 | sys.stdout.flush() 97 | sys.stdout.write('\rtest_aggregate_public_keys PASSED\n') 98 | sys.stdout.flush() 99 | #print("test_aggregate_public_keys PASSED") 100 | 101 | def test_aggregate_nonces(): 102 | for _ in range(5): 103 | nonce_secrets = [] 104 | nonces = [] 105 | aggregated_nonces = [None for _ in range(m2.nu)] 106 | for _ in range(5): 107 | nonce = b'' 108 | for ind in range(m2.nu): 109 | r_1j = m2.seckey_gen() 110 | R_1j = m2.pubkey_gen(r_1j, compressed = True) 111 | R_1j_check = m2.point_mul(m2.G, m2.int_from_bytes(r_1j)) 112 | assert R_1j_check == m2.lift_x(R_1j) 113 | nonce_secrets.append(r_1j) 114 | nonce += R_1j 115 | aggregated_nonces[ind] = m2.point_add(aggregated_nonces[ind], R_1j_check) 116 | nonces.append(nonce) 117 | assert m2.write_bytes_list_to_hex(nonce_secrets, 'test_aggregate_nonces') 118 | nonce_secrets_check = m2.read_bytes_from_hex_list('test_aggregate_nonces') 119 | os.remove('test_aggregate_nonces') 120 | assert nonce_secrets_check == nonce_secrets 121 | aggregate_nonce_points = m2.aggregate_nonces(nonces) 122 | assert aggregate_nonce_points == aggregated_nonces 123 | sys.stdout.write('.') 124 | sys.stdout.flush() 125 | sys.stdout.write('\rtest_aggregate_nonces PASSED\n') 126 | sys.stdout.flush() 127 | 128 | def test_compute_R(): 129 | for _ in range(5): 130 | random_privkey = m2.seckey_gen() 131 | random_pubkey = m2.pubkey_gen(random_privkey) 132 | nonces = [] 133 | nonce_secrets = [] 134 | # Simulate 5 participants, each with nu nonces 135 | for _ in range(5): 136 | nonce = b'' 137 | for _ in range(m2.nu): 138 | r_1j = m2.seckey_gen() 139 | R_1j = m2.pubkey_gen(r_1j, compressed = True) 140 | nonce_secrets.append(r_1j) 141 | nonce += R_1j 142 | nonces.append(nonce) 143 | aggregate_nonce_points = m2.aggregate_nonces(nonces) 144 | aggregated_nonce_bytes = [m2.bytes_from_point(R, compressed = True) for R in aggregate_nonce_points] 145 | b = m2.hash_nonces(random_pubkey, aggregated_nonce_bytes, b'hello world') 146 | R = m2.compute_R(aggregate_nonce_points, b) 147 | 148 | secret_check = 0 149 | for p in range(5): 150 | for n in range(m2.nu): 151 | nonce_secret = m2.int_from_bytes(nonce_secrets[m2.nu*p + n]) 152 | if not m2.has_even_y(R): 153 | nonce_secret = m2.n - nonce_secret 154 | secret_check += nonce_secret * (b**n) 155 | secret_check %= m2.n 156 | 157 | R_check = m2.point_mul(m2.G, secret_check) 158 | assert m2.bytes_from_point(R) == m2.bytes_from_point(R_check) 159 | 160 | sys.stdout.write('.') 161 | sys.stdout.flush() 162 | sys.stdout.write('\rtest_compute_R PASSED\n') 163 | sys.stdout.flush() 164 | 165 | def test_compute_s(): 166 | for _ in range(10): 167 | random_privkey = m2.seckey_gen() 168 | random_pubkey = m2.pubkey_gen(random_privkey) 169 | random_chall = random.randint(1, m2.n - 1) 170 | random_a_1 = random.randint(1, m2.n - 1) 171 | random_b = random.randint(1, m2.n - 1) 172 | our_R = None 173 | nonce_secrets = [] 174 | for j in range(m2.nu): 175 | r_1j = m2.seckey_gen() 176 | nonce_secrets.append(r_1j) 177 | R_1j = m2.pubkey_gen(r_1j) 178 | bj_R_1j = m2.point_mul(m2.lift_x(R_1j), (random_b**j)%m2.n) 179 | our_R = m2.point_add(our_R, bj_R_1j) 180 | s = m2.compute_s(random_chall, random_privkey, random_a_1, nonce_secrets, random_b) 181 | S = m2.point_mul(m2.G, s) 182 | a_1_pubkey = m2.point_mul(m2.lift_x(random_pubkey), random_a_1) 183 | c_a_1_pubkey = m2.point_mul(a_1_pubkey, random_chall) 184 | S_check = m2.point_add(c_a_1_pubkey, our_R) 185 | assert S == S_check 186 | 187 | sys.stdout.write('.') 188 | sys.stdout.flush() 189 | sys.stdout.write('\rtest_compute_s PASSED\n') 190 | sys.stdout.flush() 191 | 192 | 193 | if __name__ == "__main__": 194 | test_seckey_gen() 195 | test_read_write_bytes() 196 | test_point_serialisation() 197 | test_aggregate_public_keys() 198 | test_aggregate_nonces() 199 | test_compute_R() 200 | test_compute_s() 201 | --------------------------------------------------------------------------------