From 0705266889d0c122fa70cfdd6a3164c3784bf3cd Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 19:04:43 +0530 Subject: [PATCH 01/31] fix: forcefully replace cmake relic template in Autotools builds It's a problem since Autoconf 2.73 --- autogen.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/autogen.sh b/autogen.sh index de4600499..f3e943e16 100755 --- a/autogen.sh +++ b/autogen.sh @@ -10,3 +10,4 @@ fi command -v autoreconf >/dev/null || \ (echo "configuration failed, please install autoconf first" && exit 1) autoreconf --install --force --warnings=all +autoheader --force --replace-handwritten || autoheader --force From 5f30f109551800508762d83e893ca99464a167f7 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:26:47 +0530 Subject: [PATCH 02/31] doc: shave down README to the bare essentials A lot of the descriptions are out of date, include example code that cannot be validated as correct API use through CI and has still links to our parent that we've fairly diverged from. --- README.md | 318 +++++++----------------------------------------------- 1 file changed, 37 insertions(+), 281 deletions(-) diff --git a/README.md b/README.md index 8460ac818..89d07add4 100644 --- a/README.md +++ b/README.md @@ -1,299 +1,55 @@ -# BLS Signatures implementation +[![GitHub License](https://img.shields.io/github/license/dashpay/bls-signatures)](https://github.com/dashpay/bls-signatures/blob/main/LICENSE) +[![Library status](https://img.shields.io/github/actions/workflow/status/dashpay/bls-signatures/build-test.yaml?branch=main&style=flat&logo=github&logoColor=white&label=library)](https://github.com/dashpay/bls-signatures/actions/workflows/build-test.yaml?query=branch%3Amain) +[![Binds status](https://img.shields.io/github/actions/workflow/status/dashpay/bls-signatures/build-binds.yml?branch=main&style=flat&logo=github&logoColor=white&label=binds)](https://github.com/dashpay/bls-signatures/actions/workflows/build-binds.yml?query=branch%3Amain) -[![Build and Test C++, Javascript, and Python](https://github.com/Chia-Network/bls-signatures/actions/workflows/build-test.yaml/badge.svg)](https://github.com/Chia-Network/bls-signatures/actions/workflows/build-test.yaml) -![PyPI](https://img.shields.io/pypi/v/blspy?logo=pypi) -![PyPI - Format](https://img.shields.io/pypi/format/blspy?logo=pypi) -![GitHub](https://img.shields.io/github/license/Chia-Network/bls-signatures?logo=Github) - -[![Total alerts](https://img.shields.io/lgtm/alerts/g/Chia-Network/bls-signatures.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Chia-Network/bls-signatures/alerts/) -[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/Chia-Network/bls-signatures.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Chia-Network/bls-signatures/context:javascript) -[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/Chia-Network/bls-signatures.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Chia-Network/bls-signatures/context:python) -[![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/Chia-Network/bls-signatures.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Chia-Network/bls-signatures/context:cpp) - -NOTE: THIS LIBRARY IS NOT YET FORMALLY REVIEWED FOR SECURITY - -NOTE: THIS LIBRARY WAS SHIFTED TO THE IETF BLS SPECIFICATION ON 7/16/20 - -Implements BLS signatures with aggregation using [relic toolkit](https://github.com/relic-toolkit/relic) -for cryptographic primitives (pairings, EC, hashing) according to the -[IETF BLS RFC](https://datatracker.ietf.org/doc/draft-irtf-cfrg-bls-signature/) -with [these curve parameters](https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/) -for BLS12-381. - -Features: - -* Non-interactive signature aggregation following IETF specification -* Works on Windows, Mac, Linux, BSD -* Efficient verification using Proof of Posssesion (only one pairing per distinct message) -* Aggregate public keys and private keys -* [EIP-2333](https://eips.ethereum.org/EIPS/eip-2333) key derivation (including unhardened BIP-32-like keys) -* Key and signature serialization -* Batch verification -* [Python bindings](https://github.com/Chia-Network/bls-signatures/tree/main/python-bindings) -* [Pure python bls12-381 and signatures](https://github.com/Chia-Network/bls-signatures/tree/main/python-impl) -* [JavaScript bindings](https://github.com/Chia-Network/bls-signatures/tree/main/js-bindings) - -## Before you start - -This library uses minimum public key sizes (MPL). A G2Element is a signature (96 bytes), and a G1Element is a public key (48 bytes). A private key is a 32 byte integer. There are three schemes: Basic, Augmented, and ProofOfPossession. Augmented should be enough for most use cases, and ProofOfPossession can be used where verification must be fast. - -## Import the library - -```c++ -#include "bls.hpp" -using namespace bls; -``` - -## Creating keys and signatures - -```c++ -// Example seed, used to generate private key. Always use -// a secure RNG with sufficient entropy to generate a seed (at least 32 bytes). -vector seed = {0, 50, 6, 244, 24, 199, 1, 25, 52, 88, 192, - 19, 18, 12, 89, 6, 220, 18, 102, 58, 209, 82, - 12, 62, 89, 110, 182, 9, 44, 20, 254, 22}; - -PrivateKey sk = AugSchemeMPL().KeyGen(seed); -G1Element pk = sk.GetG1Element(); - -vector message = {1, 2, 3, 4, 5}; // Message is passed in as a byte vector -G2Element signature = AugSchemeMPL().Sign(sk, message); - -// Verify the signature -bool ok = AugSchemeMPL().Verify(pk, message, signature); -``` - -## Serializing keys and signatures to bytes - -```c++ -vector skBytes = sk.Serialize(); -vector pkBytes = pk.Serialize(); -vector signatureBytes = signature.Serialize(); - -cout << Util::HexStr(skBytes) << endl; // 32 bytes printed in hex -cout << Util::HexStr(pkBytes) << endl; // 48 bytes printed in hex -cout << Util::HexStr(signatureBytes) << endl; // 96 bytes printed in hex -``` - -## Loading keys and signatures from bytes - -```c++ -// Takes vector of 32 bytes -PrivateKey skc = PrivateKey::FromByteVector(skBytes); - -// Takes vector of 48 bytes -pk = G1Element::FromByteVector(pkBytes); - -// Takes vector of 96 bytes -signature = G2Element::FromByteVector(signatureBytes); -``` - -## Create aggregate signatures - -```c++ -// Generate some more private keys -seed[0] = 1; -PrivateKey sk1 = AugSchemeMPL().KeyGen(seed); -seed[0] = 2; -PrivateKey sk2 = AugSchemeMPL().KeyGen(seed); -vector message2 = {1, 2, 3, 4, 5, 6, 7}; - -// Generate first sig -G1Element pk1 = sk1.GetG1Element(); -G2Element sig1 = AugSchemeMPL().Sign(sk1, message); - -// Generate second sig -G1Element pk2 = sk2.GetG1Element(); -G2Element sig2 = AugSchemeMPL().Sign(sk2, message2); - -// Signatures can be non-interactively combined by anyone -G2Element aggSig = AugSchemeMPL().Aggregate({sig1, sig2}); - -ok = AugSchemeMPL().AggregateVerify({pk1, pk2}, {message, message2}, aggSig); -``` - -## Arbitrary trees of aggregates - -```c++ -seed[0] = 3; -PrivateKey sk3 = AugSchemeMPL().KeyGen(seed); -G1Element pk3 = sk3.GetG1Element(); -vector message3 = {100, 2, 254, 88, 90, 45, 23}; -G2Element sig3 = AugSchemeMPL().Sign(sk3, message3); - - -G2Element aggSigFinal = AugSchemeMPL().Aggregate({aggSig, sig3}); -ok = AugSchemeMPL().AggregateVerify({pk1, pk2, pk3}, {message, message2, message3}, aggSigFinal); - -``` - -## Very fast verification with Proof of Possession scheme - -```c++ -// If the same message is signed, you can use Proof of Posession (PopScheme) for efficiency -// A proof of possession MUST be passed around with the PK to ensure security. - -G2Element popSig1 = PopSchemeMPL().Sign(sk1, message); -G2Element popSig2 = PopSchemeMPL().Sign(sk2, message); -G2Element popSig3 = PopSchemeMPL().Sign(sk3, message); -G2Element pop1 = PopSchemeMPL().PopProve(sk1); -G2Element pop2 = PopSchemeMPL().PopProve(sk2); -G2Element pop3 = PopSchemeMPL().PopProve(sk3); - -ok = PopSchemeMPL().PopVerify(pk1, pop1); -ok = PopSchemeMPL().PopVerify(pk2, pop2); -ok = PopSchemeMPL().PopVerify(pk3, pop3); -G2Element popSigAgg = PopSchemeMPL().Aggregate({popSig1, popSig2, popSig3}); +> [!WARNING] +> +> It is heavily advised **against** using this library for new consensus implementations and to use established +> spec-conformant libraries like [supranational/blst](https://github.com/supranational/blst) as this library codifies +> primitives predating the final IETF spec and includes a non-standard (now legacy) scheme. +> +> **This library has not undergone a formal security review.** -ok = PopSchemeMPL().FastAggregateVerify({pk1, pk2, pk3}, message, popSigAgg); +`bls-signatures` is a cross-platform library implementing BLS12-381 primitives for Dash built on +the [`relic`](https://github.com/relic-toolkit/relic) toolkit with bindings available in +[Python](./python-bindings), [Rust](./rust-bindings/), [Go](./go-bindings/) and [Javascript](./js-bindings). -// Aggregate public key, indistinguishable from a single public key -G1Element popAggPk = pk1 + pk2 + pk3; -ok = PopSchemeMPL().Verify(popAggPk, message, popSigAgg); +## Dependencies -// Aggregate private keys -PrivateKey aggSk = PrivateKey::Aggregate({sk1, sk2, sk3}); -ok = (PopSchemeMPL().Sign(aggSk, message) == popSigAgg); -``` +* A C++17 capable compiler (GCC 9, Clang 7 or higher) +* CMake 3.18 or higher (or Autoconf 2.71 or higher; with libtool and automake) +* [`libgmp`](https://gmplib.org/) (for fast arithmetic, **optional**) -## HD keys using [EIP-2333](https://github.com/ethereum/EIPs/pull/2333) +Additionally, the following dependencies are supplied by the codebase -```c++ -// You can derive 'child' keys from any key, to create arbitrary trees. 4 byte indeces are used. -// Hardened (more secure, but no parent pk -> child pk) -PrivateKey masterSk = AugSchemeMPL().KeyGen(seed); -PrivateKey child = AugSchemeMPL().DeriveChildSk(masterSk, 152); -PrivateKey grandChild = AugSchemeMPL().DeriveChildSk(child, 952) +* [`catch2`](https://github.com/catchorg/Catch2) (for tests) +* [`mimalloc`](https://github.com/microsoft/mimalloc) (for secure memory operations) +* [`relic`](https://github.com/relic-toolkit/relic) (for cryptographic operations) -// Unhardened (less secure, but can go from parent pk -> child pk), BIP32 style -G1Element masterPk = masterSk.GetG1Element(); -PrivateKey childU = AugSchemeMPL().DeriveChildSkUnhardened(masterSk, 22); -PrivateKey grandchildU = AugSchemeMPL().DeriveChildSkUnhardened(childU, 0); +## Build library -G1Element childUPk = AugSchemeMPL().DeriveChildPkUnhardened(masterPk, 22); -G1Element grandchildUPk = AugSchemeMPL().DeriveChildPkUnhardened(childUPk, 0); +```sh +# Create scratchpad directory +mkdir build && cd build -ok = (grandchildUPk == grandchildU.GetG1Element(); -``` +# Configure build files +cmake .. -## Build +# Build library with 4 threads +cmake --build . --parallel 4 -Cmake 3.14+, a c++ compiler, and python3 (for bindings) are required for building. +# Run tests +./src/runtest -```bash -mkdir build -cd build -cmake ../ -cmake --build . -- -j 6 +# Run benchmarks +./src/runbench ``` -### Run tests - -```bash -./build/src/runtest ``` -### Run benchmarks +## License -```bash -./build/src/runbench +```text +Copyright (c) 2018-present, Chia Network, Inc. +Copyright (c) 2021-present, The Dash Core developers. ``` - -On a 3.5 GHz i7 Mac, verification takes about 1.1ms per signature, and signing takes 1.3ms. - -### Link the library to use it - -```bash -g++ -Wl,-no_pie -std=c++11 -Ibls-signatures/depends/relic/include -Ibls-signatures/build/depends/relic/include -Ibls-signatures/src -L./bls-signatures/build/ -l bls yourapp.cpp -``` - -## Notes on dependencies - -We use Libsodium and have GMP as an optional dependency: libsodium gives secure memory -allocation, and GMP speeds up the library by ~ 3x. MPIR is used on Windows via -GitHub Actions instead. To install them, either download them from github and -follow the instructions for each repo, or use a package manager like APT or -brew. You can follow the recipe used to build python wheels for multiple -platforms in `.github/workflows/`. - -## Discussion - -Discussion about this library and other Chia related development is in the #dev -channel of Chia's [public Keybase channels](https://keybase.io/team/chia_network.public). - -## Code style - -* Always use vector for bytes -* Use size_t for size variables -* Uppercase method names -* Prefer static constructors -* Avoid using templates -* Objects allocate and free their own memory -* Use cpplint with default rules -* Use SecAlloc and SecFree when handling secrets - -## ci Building - -The primary build process for this repository is to use GitHub Actions to -build binary wheels for MacOS, Linux (x64 and aarch64), and Windows and publish -them with a source wheel on PyPi. MacOS ARM64 is supported but not automated -due to a lack of M1 CI runners. See `.github/workflows/build.yml`. CMake uses -[FetchContent](https://cmake.org/cmake/help/latest/module/FetchContent.html) -to download [pybind11](https://github.com/pybind/pybind11) for the Python -bindings and relic from a chia relic forked repository for Windows. Building -is then managed by [cibuildwheel](https://github.com/joerick/cibuildwheel). -Further installation is then available via `pip install blspy` e.g. The ci -builds include GMP and a statically linked libsodium. - -## Contributing and workflow - -Contributions are welcome and more details are available in chia-blockchain's -[CONTRIBUTING.md](https://github.com/Chia-Network/chia-blockchain/blob/main/CONTRIBUTING.md). - -The main branch is usually the currently released latest version on PyPI. -Note that at times bls-signatures/blspy will be ahead of the release version -that chia-blockchain requires in it's main/release version in preparation -for a new chia-blockchain release. Please branch or fork main and then create -a pull request to the main branch. Linear merging is enforced on main and -merging requires a completed review. PRs will kick off a GitHub actions ci -build and analysis of bls-signatures at -[lgtm.com](https://lgtm.com/projects/g/Chia-Network/bls-signatures/?mode=list). -Please make sure your build is passing and that it does not increase alerts -at lgtm. - -## Specification and test vectors - -The [IETF bls draft](https://datatracker.ietf.org/doc/draft-irtf-cfrg-hash-to-curve/) -is followed. Test vectors can also be seen in the python and cpp test files. - -## Libsodium license - -The libsodium static library is licensed under the ISC license which requires -the following copyright notice. - ->ISC License -> ->Copyright (c) 2013-2020 ->Frank Denis \ -> ->Permission to use, copy, modify, and/or distribute this software for any ->purpose with or without fee is hereby granted, provided that the above ->copyright notice and this permission notice appear in all copies. -> ->THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES ->WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF ->MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ->ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES ->WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ->ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF ->OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - -## GMP license - -GMP is distributed under the -[GNU LGPL v3 license](https://www.gnu.org/licenses/lgpl-3.0.html) - -## Relic license - -Relic is used with the -[Apache 2.0 license](https://github.com/relic-toolkit/relic/blob/master/LICENSE.Apache-2.0) From 013c91a83dccc3e17e35cf9593cf0e330462b842 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:49:53 +0530 Subject: [PATCH 03/31] chore: drop unmaintained native Python implementation --- .flake8 | 4 - .github/workflows/build-binds.yml | 11 +- lgtm.yml | 12 - mypi.ini | 2 - python-impl/README.md | 12 - python-impl/bls12381.py | 101 ---- python-impl/ec.py | 586 ----------------------- python-impl/fields.py | 763 ------------------------------ python-impl/hash_to_field.py | 99 ---- python-impl/hd_keys.py | 90 ---- python-impl/hkdf.py | 53 --- python-impl/impl-test.py | 663 -------------------------- python-impl/op_swu_g2.py | 214 --------- python-impl/pairing.py | 136 ------ python-impl/private_key.py | 81 ---- python-impl/schemes.py | 209 -------- python-impl/util.py | 49 -- 17 files changed, 1 insertion(+), 3084 deletions(-) delete mode 100644 .flake8 delete mode 100644 lgtm.yml delete mode 100644 mypi.ini delete mode 100644 python-impl/README.md delete mode 100644 python-impl/bls12381.py delete mode 100644 python-impl/ec.py delete mode 100644 python-impl/fields.py delete mode 100644 python-impl/hash_to_field.py delete mode 100644 python-impl/hd_keys.py delete mode 100644 python-impl/hkdf.py delete mode 100644 python-impl/impl-test.py delete mode 100644 python-impl/op_swu_g2.py delete mode 100644 python-impl/pairing.py delete mode 100644 python-impl/private_key.py delete mode 100644 python-impl/schemes.py delete mode 100644 python-impl/util.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index ccb153056..000000000 --- a/.flake8 +++ /dev/null @@ -1,4 +0,0 @@ -[flake8] -max-line-length = 120 -exclude = ./typings/**/* python-impl/fields.py -ignore = E203,W503,E501 diff --git a/.github/workflows/build-binds.yml b/.github/workflows/build-binds.yml index 43dc8b212..3c2dd69ce 100644 --- a/.github/workflows/build-binds.yml +++ b/.github/workflows/build-binds.yml @@ -17,28 +17,19 @@ concurrency: jobs: build: - name: ${{ matrix.os }}, Python ${{ matrix.python }}, Go ${{ matrix.golang }}, Rust ${{ matrix.rust }} + name: ${{ matrix.os }}, Go ${{ matrix.golang }}, Rust ${{ matrix.rust }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: [macos-latest, ubuntu-latest] golang: [ '1.24' ] - python: ['3.10', '3.11', '3.12', '3.13'] rust: [ '1.91.0' ] steps: - name: Checkout code uses: actions/checkout@v3 - - uses: chia-network/actions/setup-python@main - with: - python-version: ${{ matrix.python }} - - - name: Test Python implementation - run: | - python python-impl/impl-test.py - - name: Install Emscripten SDK uses: mymindstorm/setup-emsdk@v11 diff --git a/lgtm.yml b/lgtm.yml deleted file mode 100644 index e8d3f8a20..000000000 --- a/lgtm.yml +++ /dev/null @@ -1,12 +0,0 @@ -extraction: - cpp: - after_prepare: - - "mkdir custom_cmake" - - "wget --quiet -O - \"https://cmake.org/files/v3.16/cmake-3.16.3-Linux-x86_64.tar.gz\"\ - \ | tar --strip-components=1 -xz -C custom_cmake" - - "export PATH=$(pwd)/custom_cmake/bin:${PATH}" - - "cd $LGTM_SRC/" - - "export CMAKE_INCLUDE_PATH=$LGTM_SRC/include:${CMAKE_INCLUDE_PATH}" - - "export CMAKE_LIBRARY_PATH=$LGTM_SRC/lib:${CMAKE_LIBRARY_PATH}" - - "mkdir $LGTM_SRC/_lgtm_build_dir" - - "cd $LGTM_SRC/_lgtm_build_dir" diff --git a/mypi.ini b/mypi.ini deleted file mode 100644 index 976ba0294..000000000 --- a/mypi.ini +++ /dev/null @@ -1,2 +0,0 @@ -[mypy] -ignore_missing_imports = True diff --git a/python-impl/README.md b/python-impl/README.md deleted file mode 100644 index 4ee657903..000000000 --- a/python-impl/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# BLS12-381 and Signatures in python - -Implements the BLS12 curve and optimal ate pairing, as well -as BLS signatures and aggregation. Use for reference / educational purposes only. - -For an optimized implementation, use the [Python bindings](https://github.com/Chia-Network/bls-signatures/tree/main/python-bindings). - -For a good introduction to pairings, read [Pairings for Beginners](https://static1.squarespace.com/static/5fdbb09f31d71c1227082339/t/5ff394720493bd28278889c6/1609798774687/PairingsForBeginners.pdf) by Craig Costello. - -Map to curve implementation from [Algorand](https://github.com/algorand/bls_sigs_ref/). - -Run the tests with `python impl-test.py`. diff --git a/python-impl/bls12381.py b/python-impl/bls12381.py deleted file mode 100644 index c3a16fe22..000000000 --- a/python-impl/bls12381.py +++ /dev/null @@ -1,101 +0,0 @@ -# flake8: noqa -from fields import Fq, Fq2 - -# BLS parameter used to generate the other parameters -# Spec is found here: https://github.com/zkcrypto/pairing/tree/master/src/bls12_381 -x = -0xD201000000010000 - -# 381 bit prime -# Also see fields:bls12381_q -q = 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAAAB - -# a,b and a2, b2, define the elliptic curve and twisted curve. -# y^2 = x^3 + 4 -# y^2 = x^3 + 4(u + 1) -a = Fq(q, 0) -b = Fq(q, 4) -a_twist = Fq2(q, 0, 0) -b_twist = Fq2(q, 4, 4) - -# The generators for g1 and g2 -gx = Fq( - q, - 0x17F1D3A73197D7942695638C4FA9AC0FC3688C4F9774B905A14E3A3F171BAC586C55E83FF97A1AEFFB3AF00ADB22C6BB, -) -gy = Fq( - q, - 0x08B3F481E3AAA0F1A09E30ED741D8AE4FCF5E095D5D00AF600DB18CB2C04B3EDD03CC744A2888AE40CAA232946C5E7E1, -) - -g2x = Fq2( - q, - 352701069587466618187139116011060144890029952792775240219908644239793785735715026873347600343865175952761926303160, - 3059144344244213709971259814753781636986470325476647558659373206291635324768958432433509563104347017837885763365758, -) -g2y = Fq2( - q, - 1985150602287291935568054521177171638300868978215655730859378665066344726373823718423869104263333984641494340347905, - 927553665492332455747201965776037880757740193453592970025027978793976877002675564980949289727957565575433344219582, -) - -# The order of all three groups (g1, g2, and gt). Note, the elliptic curve E_twist -# actually has more valid points than this. This is relevant when hashing onto the -# curve, where we use a point that is not in g2, and map it into g2. -n = 0x73EDA753299D7D483339D80809A1D80553BDA402FFFE5BFEFFFFFFFF00000001 - -# Cofactor used to generate r torsion points -h = 0x396C8C005555E1568C00AAAB0000AAAB - -# https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-07#section-8.8.2 -h_eff = 0xBC69F08F2EE75B3584C6A0EA91B352888E2A8E9145AD7689986FF031508FFE1329C2F178731DB956D82BF015D1212B02EC0EC69D7477C1AE954CBC06689F6A359894C0ADEBBF6B4E8020005AAA95551 - -# Embedding degree -k = 12 - -# sqrt(-3) mod q -sqrt_n3 = 1586958781458431025242759403266842894121773480562120986020912974854563298150952611241517463240701 - -# (sqrt(-3) - 1) / 2 mod q -sqrt_n3m1o2 = 793479390729215512621379701633421447060886740281060493010456487427281649075476305620758731620350 - -# This is the normal elliptic curve. G1 points are on here. -def parameters(): - return (q, a, b, gx, gy, g2x, g2y, n, h, x, k, sqrt_n3, sqrt_n3m1o2) - - -# This is the sextic twist used to send elements of G2 from -# coordinates in Fq12 to coordinates in Fq2. It's isomorphic -# to the above elliptic curve. See Page 63 of Costello. -def parameters_twist(): - return ( - q, - a_twist, - b_twist, - gx, - gy, - g2x, - g2y, - n, - h_eff, - x, - k, - sqrt_n3, - sqrt_n3m1o2, - ) - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/ec.py b/python-impl/ec.py deleted file mode 100644 index 36f573d11..000000000 --- a/python-impl/ec.py +++ /dev/null @@ -1,586 +0,0 @@ -from __future__ import annotations - -from collections import namedtuple -from copy import deepcopy -from typing import List, Optional - -import bls12381 -from fields import FieldExtBase, Fq, Fq2, Fq6, Fq12 -from util import hash256 - -# Struct for elliptic curve parameters -EC = namedtuple("EC", "q a b gx gy g2x g2y n h x k sqrt_n3 sqrt_n3m1o2") - -default_ec = EC(*bls12381.parameters()) -default_ec_twist = EC(*bls12381.parameters_twist()) - - -class AffinePoint: - """ - Elliptic curve point, can represent any curve, and use Fq or Fq2 - coordinates. - """ - - def __init__(self, x, y, infinity: bool, ec=default_ec): - if ( - (not isinstance(x, Fq) and not isinstance(x, FieldExtBase)) - or (not isinstance(y, Fq) and not isinstance(y, FieldExtBase)) - or type(x) != type(y) - ): - raise Exception("x,y should be field elements") - self.FE = type(x) - self.x = x - self.y = y - self.infinity = infinity - self.ec = ec - - def is_on_curve(self) -> bool: - """ - Check that y^2 = x^3 + ax + b. - """ - if self.infinity: - return True - left = self.y * self.y - right = self.x * self.x * self.x + self.ec.a * self.x + self.ec.b - - return left == right - - def __add__(self, other: AffinePoint) -> AffinePoint: - if other == 0: - return self - if not isinstance(other, AffinePoint): - raise Exception("Incorrect object") - - return add_points(self, other, self.ec, self.FE) - - def __radd__(self, other: AffinePoint) -> AffinePoint: - return self.__add__(other) - - def __sub__(self, other: AffinePoint) -> AffinePoint: - return self.__add__(other.negate()) - - def __rsub__(self, other: AffinePoint) -> AffinePoint: - return self.negate().__add__(other) - - def __str__(self) -> str: - return ( - "AffinePoint(x=" - + self.x.__str__() - + ", y=" - + self.y.__str__() - + ", i=" - + str(self.infinity) - + ")\n" - ) - - def __repr__(self) -> str: - return ( - "AffinePoint(x=" - + self.x.__repr__() - + ", y=" - + self.y.__repr__() - + ", i=" - + str(self.infinity) - + ")\n" - ) - - def __eq__(self, other) -> bool: - if not isinstance(other, AffinePoint): - return False - return ( - self.x == other.x and self.y == other.y and self.infinity == other.infinity - ) - - def __ne__(self, other) -> bool: - return not self.__eq__(other) - - def __mul__(self, c) -> AffinePoint: - if not isinstance(c, Fq) and not isinstance(c, int): - raise ValueError("Error, must be int or Fq") - return scalar_mult_jacobian(c, self.to_jacobian(), self.ec).to_affine() - - def negate(self) -> AffinePoint: - return AffinePoint(self.x, -self.y, self.infinity, self.ec) - - def __rmul__(self, c: Fq) -> AffinePoint: - return self.__mul__(c) - - def to_jacobian(self) -> JacobianPoint: - return JacobianPoint( - self.x, self.y, self.FE.one(self.ec.q), self.infinity, self.ec - ) - - def __deepcopy__(self, memo) -> AffinePoint: - return AffinePoint( - deepcopy(self.x, memo), deepcopy(self.y, memo), self.infinity, self.ec - ) - - -class JacobianPoint: - """ - Elliptic curve point, can represent any curve, and use Fq or Fq2 - coordinates. Uses Jacobian coordinates so that point addition - does not require slow inversion. - """ - - def __init__(self, x, y, z, infinity: bool, ec=default_ec): - - if ( - not isinstance(x, Fq) - and not isinstance(x, FieldExtBase) - or (not isinstance(y, Fq) and not isinstance(y, FieldExtBase)) - or (not isinstance(z, Fq) and not isinstance(z, FieldExtBase)) - ): - raise Exception("x,y should be field elements") - self.FE = type(x) - self.x = x - self.y = y - self.z = z - self.infinity = infinity - self.ec = ec - - def is_on_curve(self) -> bool: - if self.infinity: - return True - return self.to_affine().is_on_curve() - - def negate(self) -> JacobianPoint: - return self.to_affine().negate().to_jacobian() - - def to_affine(self) -> AffinePoint: - if self.infinity: - return AffinePoint( - Fq.zero(self.ec.q), Fq.zero(self.ec.q), self.infinity, self.ec - ) - new_x = self.x / (self.z ** 2) - new_y = self.y / (self.z ** 3) - return AffinePoint(new_x, new_y, self.infinity, self.ec) - - def check_valid(self) -> None: - assert self.is_on_curve() - assert self * self.ec.n == G2Infinity() - - def get_fingerprint(self) -> int: - ser = bytes(self) - return int.from_bytes(hash256(ser)[:4], "big") - - def __add__(self, other: JacobianPoint) -> JacobianPoint: - if other == 0: - return self - if not isinstance(other, JacobianPoint): - raise ValueError("Incorrect object") - - return add_points_jacobian(self, other, self.ec, self.FE) - - def __radd__(self, other: JacobianPoint) -> JacobianPoint: - return self.__add__(other) - - def __eq__(self, other) -> bool: - if not isinstance(other, JacobianPoint): - return False - return self.to_affine() == other.to_affine() - - def __ne__(self, other) -> bool: - return not self.__eq__(other) - - def __mul__(self, c) -> JacobianPoint: - if not isinstance(c, int) and not isinstance(c, Fq): - raise ValueError("Error, must be int or Fq") - return scalar_mult_jacobian(c, self, self.ec) - - def __rmul__(self, c) -> JacobianPoint: - return self.__mul__(c) - - def __neg__(self) -> JacobianPoint: - return self.to_affine().negate().to_jacobian() - - def __str__(self) -> str: - return ( - "JacobianPoint(x=" - + self.x.__str__() - + ", y=" - + self.y.__str__() - + "z=" - + self.z.__str__() - + ", i=" - + str(self.infinity) - + ")\n" - ) - - def __repr__(self) -> str: - return self.__str__() - - def __bytes__(self) -> bytes: - return point_to_bytes(self, self.ec, self.FE) - - def __deepcopy__(self, memo) -> JacobianPoint: - return JacobianPoint( - deepcopy(self.x, memo), - deepcopy(self.y, memo), - deepcopy(self.z, memo), - self.infinity, - self.ec, - ) - - def __hash__(self) -> int: - return int.from_bytes(bytes(self), "big") - - -def sign_Fq(element, ec=default_ec) -> bool: - return element > Fq(ec.q, ((ec.q - 1) // 2)) - - -def sign_Fq2(element, ec=default_ec_twist) -> bool: - if element[1] == Fq(ec.q, 0): - return sign_Fq(element[0]) - - return element[1] > Fq(ec.q, ((ec.q - 1) // 2)) - - -def point_to_bytes(point_j: JacobianPoint, ec, FE) -> bytes: - # Zcash serialization described in https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/ - point = point_j.to_affine() - output = bytearray(bytes(point.x)) - - # If the y coordinate is the bigger one of the two, set the first - # bit to 1. - if point.infinity: - return bytes([0x40]) + bytes([0] * (len(output) - 1)) - - if FE == Fq: - sign = sign_Fq(point.y, ec) - else: - sign = sign_Fq2(point.y, ec) - - if sign: - output[0] |= 0xA0 - else: - output[0] |= 0x80 - return bytes(output) - - -def bytes_to_point(buffer: bytes, ec, FE) -> JacobianPoint: - # Zcash deserialization described in https://datatracker.ietf.org/doc/draft-irtf-cfrg-pairing-friendly-curves/ - - if FE == Fq: - if len(buffer) != 48: - raise ValueError("G1Elements must be 48 bytes") - elif FE == Fq2: - if len(buffer) != 96: - raise ValueError("G2Elements must be 96 bytes") - else: - raise ValueError("Invalid FE") - - m_byte = buffer[0] & 0xE0 - - if m_byte in [0x20, 0x60, 0xE0]: - raise ValueError("Invalid first three bits") - - C_bit = (m_byte & 0x80) >> 7 # First bit - I_bit = (m_byte & 0x40) >> 6 # Second bit - S_bit = (m_byte & 0x20) >> 5 # Third bit - - if C_bit == 0: - raise ValueError("First bit must be 1 (only compressed points)") - - buffer = bytes([buffer[0] & 0x1F]) + buffer[1:] - - if I_bit == 1: - if any([e != 0 for e in buffer]): - raise ValueError("Point at infinity set, but data not all zeroes") - return AffinePoint(FE.zero(ec.q), FE.zero(ec.q), True, ec).to_jacobian() - - x = FE.from_bytes(buffer, ec.q) - y_value = y_for_x(x, ec, FE) - - if FE == Fq: - sign_fn = sign_Fq - else: - sign_fn = sign_Fq2 - - if sign_fn(y_value, ec) == S_bit: - y = y_value - else: - y = -y_value - - return AffinePoint(x, y, False, ec).to_jacobian() - - -def y_for_x(x, ec=default_ec, FE=Fq): - """ - Solves y = sqrt(x^3 + ax + b) for both valid ys. - """ - if not isinstance(x, FE): - x = FE(ec.q, x) - - u = x * x * x + ec.a * x + ec.b - - y = u.modsqrt() - if y == 0 or not AffinePoint(x, y, False, ec).is_on_curve(): - raise ValueError("No y for point x") - return y - - -def double_point(p1: AffinePoint, ec=default_ec, FE=Fq) -> AffinePoint: - """ - Basic elliptic curve point doubling - """ - x, y = p1.x, p1.y - left = Fq(ec.q, 3) * x * x - left = left + ec.a - s = left / (Fq(ec.q, 2) * y) - new_x = s * s - x - x - new_y = s * (x - new_x) - y - return AffinePoint(new_x, new_y, False, ec) - - -def add_points(p1: AffinePoint, p2: AffinePoint, ec=default_ec, FE=Fq) -> AffinePoint: - """ - Basic elliptic curve point addition. - """ - assert p1.is_on_curve() - assert p2.is_on_curve() - if p1.infinity: - return p2 - if p2.infinity: - return p1 - if p1 == p2: - return double_point(p1, ec, FE) - if p1.x == p2.x: - return AffinePoint(FE.zero(ec.q), FE.zero(ec.q), True, ec) - - x1, y1 = p1.x, p1.y - x2, y2 = p2.x, p2.y - s = (y2 - y1) / (x2 - x1) - new_x = s * s - x1 - x2 - new_y = s * (x1 - new_x) - y1 - return AffinePoint(new_x, new_y, False, ec) - - -def double_point_jacobian(p1: JacobianPoint, ec=default_ec, FE=Fq) -> JacobianPoint: - """ - Jacobian elliptic curve point doubling, see - http://www.hyperelliptic.org/EFD/oldefd/jacobian.html - """ - X, Y, Z = p1.x, p1.y, p1.z - if Y == FE.zero(ec.q) or p1.infinity: - return JacobianPoint(FE.one(ec.q), FE.one(ec.q), FE.zero(ec.q), True, ec) - - # S = 4*X*Y^2 - S = Fq(ec.q, 4) * X * Y * Y - - Z_sq = Z * Z - Z_4th = Z_sq * Z_sq - Y_sq = Y * Y - Y_4th = Y_sq * Y_sq - - # M = 3*X^2 + a*Z^4 - M = Fq(ec.q, 3) * X * X - M += ec.a * Z_4th - - # X' = M^2 - 2*S - X_p = M * M - Fq(ec.q, 2) * S - # Y' = M*(S - X') - 8*Y^4 - Y_p = M * (S - X_p) - Fq(ec.q, 8) * Y_4th - # Z' = 2*Y*Z - Z_p = Fq(ec.q, 2) * Y * Z - return JacobianPoint(X_p, Y_p, Z_p, False, ec) - - -def add_points_jacobian( - p1: JacobianPoint, p2: JacobianPoint, ec=default_ec, FE=Fq -) -> JacobianPoint: - """ - Jacobian elliptic curve point addition, see - http://www.hyperelliptic.org/EFD/oldefd/jacobian.html - """ - if p1.infinity: - return p2 - if p2.infinity: - return p1 - # U1 = X1*Z2^2 - U1 = p1.x * (p2.z ** 2) - # U2 = X2*Z1^2 - U2 = p2.x * (p1.z ** 2) - # S1 = Y1*Z2^3 - S1 = p1.y * (p2.z ** 3) - # S2 = Y2*Z1^3 - S2 = p2.y * (p1.z ** 3) - if U1 == U2: - if S1 != S2: - return JacobianPoint(FE.one(ec.q), FE.one(ec.q), FE.zero(ec.q), True, ec) - else: - return double_point_jacobian(p1, ec, FE) - - # H = U2 - U1 - H = U2 - U1 - # R = S2 - S1 - R = S2 - S1 - H_sq = H * H - H_cu = H * H_sq - # X3 = R^2 - H^3 - 2*U1*H^2 - X3 = R * R - H_cu - Fq(ec.q, 2) * U1 * H_sq - # Y3 = R*(U1*H^2 - X3) - S1*H^3 - Y3 = R * (U1 * H_sq - X3) - S1 * H_cu - # Z3 = H*Z1*Z2 - Z3 = H * p1.z * p2.z - return JacobianPoint(X3, Y3, Z3, False, ec) - - -def scalar_mult(c, p1: AffinePoint, ec=default_ec, FE=Fq) -> AffinePoint: - """ - Double and add, see - https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication - """ - if p1.infinity or c % ec.q == 0: - return AffinePoint(FE.zero(ec.q), FE.zero(ec.q), ec) - result = AffinePoint(FE.zero(ec.q), FE.zero(ec.q), True, ec) - addend = p1 - while c > 0: - if c & 1: - result += addend - - # double point - addend += addend - c = c >> 1 - - return result - - -def scalar_mult_jacobian(c, p1: JacobianPoint, ec=default_ec, FE=Fq) -> JacobianPoint: - """ - Double and add, see - https://en.wikipedia.org/wiki/Elliptic_curve_point_multiplication - """ - if isinstance(c, FE): - c = c.value - if p1.infinity or c % ec.q == 0: - return JacobianPoint(FE.one(ec.q), FE.one(ec.q), FE.zero(ec.q), True, ec) - - result = JacobianPoint(FE.one(ec.q), FE.one(ec.q), FE.zero(ec.q), True, ec) - addend = p1 - while c > 0: - if c & 1: - result += addend - # double point - addend += addend - c = c >> 1 - return result - - -def G1Generator(ec=default_ec) -> JacobianPoint: - return AffinePoint(ec.gx, ec.gy, False, ec).to_jacobian() - - -def G2Generator(ec=default_ec_twist) -> JacobianPoint: - return AffinePoint(ec.g2x, ec.g2y, False, ec).to_jacobian() - - -def G1Infinity(ec=default_ec, FE=Fq) -> JacobianPoint: - return JacobianPoint(FE.one(ec.q), FE.one(ec.q), FE.zero(ec.q), True, ec) - - -def G2Infinity(ec=default_ec_twist, FE=Fq2) -> JacobianPoint: - return JacobianPoint(FE.one(ec.q), FE.one(ec.q), FE.zero(ec.q), True, ec) - - -def G1FromBytes(buffer: bytes, ec=default_ec, FE=Fq) -> JacobianPoint: - return bytes_to_point(buffer, ec, FE) - - -def G2FromBytes(buffer: bytes, ec=default_ec_twist, FE=Fq2): - return bytes_to_point(buffer, ec, FE) - - -def untwist(point: AffinePoint, ec=default_ec) -> AffinePoint: - """ - Given a point on G2 on the twisted curve, this converts its - coordinates back from Fq2 to Fq12. See Craig Costello book, look - up twists. - """ - f = Fq12.one(ec.q) - wsq = Fq12(ec.q, f.root, Fq6.zero(ec.q)) - wcu = Fq12(ec.q, Fq6.zero(ec.q), f.root) - return AffinePoint(point.x / wsq, point.y / wcu, False, ec) - - -def twist(point: AffinePoint, ec=default_ec_twist) -> AffinePoint: - """ - Given an untwisted point, this converts it's - coordinates to a point on the twisted curve. See Craig Costello - book, look up twists. - """ - f = Fq12.one(ec.q) - wsq = Fq12(ec.q, f.root, Fq6.zero(ec.q)) - wcu = Fq12(ec.q, Fq6.zero(ec.q), f.root) - new_x = point.x * wsq - new_y = point.y * wcu - return AffinePoint(new_x, new_y, False, ec) - - -# Isogeny map evaluation specified by map_coeffs -# -# map_coeffs should be specified as (xnum, xden, ynum, yden) -# -# This function evaluates the isogeny over Jacobian projective coordinates. -# For details, see Section 4.3 of -# Wahby and Boneh, "Fast and simple constant-time hashing to the BLS12-381 elliptic curve." -# ePrint # 2019/403, https://ia.cr/2019/403. -def eval_iso(P: JacobianPoint, map_coeffs, ec) -> JacobianPoint: - (x, y, z) = (P.x, P.y, P.z) - mapvals: List[Optional[Fq2]] = [None] * 4 - - # Precompute the required powers of Z^2 - maxord = max(len(coeffs) for coeffs in map_coeffs) - zpows: List[Optional[Fq2]] = [None] * maxord - zpows[0] = z ** 0 # type: ignore - zpows[1] = z ** 2 # type: ignore - for idx in range(2, len(zpows)): - assert zpows[idx - 1] is not None - assert zpows[1] is not None - zpows[idx] = zpows[idx - 1] * zpows[1] - - # Compute the numerator and denominator of the X and Y maps via Horner's rule - for (idx, coeffs) in enumerate(map_coeffs): - coeffs_z = [ - zpow * c for (zpow, c) in zip(reversed(coeffs), zpows[: len(coeffs)]) - ] - tmp = coeffs_z[0] - for coeff in coeffs_z[1:]: - tmp *= x - tmp += coeff - mapvals[idx] = tmp - - # xden is of order 1 less than xnum, so one needs to multiply it by an extra factor of Z^2 - assert len(map_coeffs[1]) + 1 == len(map_coeffs[0]) - assert zpows[1] is not None - assert mapvals[1] is not None - mapvals[1] *= zpows[1] - - # Multiply the result of Y map by the y-coordinate y / z^3 - assert mapvals[2] is not None - assert mapvals[3] is not None - mapvals[2] *= y - mapvals[3] *= z ** 3 - - Z = mapvals[1] * mapvals[3] - X = mapvals[0] * mapvals[3] * Z - Y = mapvals[2] * mapvals[1] * Z * Z - return JacobianPoint(X, Y, Z, P.infinity, ec) - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/fields.py b/python-impl/fields.py deleted file mode 100644 index 418ce5073..000000000 --- a/python-impl/fields.py +++ /dev/null @@ -1,763 +0,0 @@ -from __future__ import annotations - -from copy import deepcopy -from typing import Any - - -class Fq: - """ - Represents an element of a finite field mod a prime q. - """ - - value: int - extension: int = 1 - - def __init__(self, Q: int, value: int): - self.Q = Q - self.value = value % Q - - def __neg__(self) -> Fq: - return Fq(self.Q, -self.value) - - def __add__(self, other: Fq) -> Fq: - if not isinstance(other, Fq): - return NotImplemented - return Fq(self.Q, self.value + other.value) - - def __radd__(self, other: Fq) -> Fq: - if not isinstance(other, Fq): - return NotImplemented - return self.__add__(other) - - def __sub__(self, other: Fq) -> Fq: - if not isinstance(other, Fq): - return NotImplemented - return Fq(self.Q, self.value - other.value) - - def __rsub__(self, other: Fq) -> Fq: - if not isinstance(other, Fq): - return NotImplemented - return Fq(self.Q, other.value - self.value) - - def __mul__(self, other: Fq) -> Fq: - if not isinstance(other, Fq): - return NotImplemented - return Fq(self.Q, self.value * other.value) - - def __rmul__(self, other: Fq) -> Fq: - return self.__mul__(other) - - def __eq__(self, other) -> bool: - if not isinstance(other, type(self)): - return False - else: - return self.value == other.value and self.Q == other.Q - - def __lt__(self, other: Fq) -> bool: - return self.value < other.value - - def __gt__(self, other: Fq) -> bool: - return self.value > other.value - - def __le__(self, other: Fq) -> bool: - return self.value <= other.value - - def __ge__(self, other: Fq) -> bool: - return self.value >= other.value - - def __str__(self): - s = hex(self.value) - s2 = s[0:7] + ".." + s[-5:] if len(s) > 10 else s - return "Fq(" + s2 + ")" - - def __repr__(self): - return "Fq(" + hex(self.value) + ")" - - def __bytes__(self): - return self.value.to_bytes(48, "big") - - @staticmethod - def from_bytes(buffer: bytes, q: int): - assert len(buffer) == 48 - return Fq(q, int.from_bytes(buffer, "big")) - - def __pow__(self, other) -> Fq: - if other == 0: - return Fq(self.Q, 1) - elif other == 1: - return Fq(self.Q, self.value) - elif other % 2 == 0: - return Fq(self.Q, self.value * self.value) ** (other // 2) - else: - return Fq(self.Q, self.value * self.value) ** (other // 2) * self - - def qi_power(self, i: int) -> Fq: - return self - - def __invert__(self) -> Fq: - """ - Extended euclidian algorithm for inversion. - """ - x0, x1, y0, y1 = 1, 0, 0, 1 - a = int(self.Q) - b = int(self.value) - while a != 0: - q, b, a = b // a, a, b % a - x0, x1 = x1, x0 - q * x1 - y0, y1 = y1, y0 - q * y1 - return Fq(self.Q, x0) - - def __floordiv__(self, other) -> Fq: - if isinstance(other, int) and not isinstance(other, type(self)): - other = Fq(self.Q, other) - return self * ~other - - __truediv__ = __floordiv__ - - def __iter__(self): - yield self - - def modsqrt(self) -> Fq: - if int(self.value) == 0: - return Fq(self.Q, 0) - if pow(int(self.value), (self.Q - 1) // 2, self.Q) != 1: - raise ValueError("No sqrt exists") - if self.Q % 4 == 3: - return Fq(self.Q, pow(int(self.value), (self.Q + 1) // 4, self.Q)) - if self.Q % 8 == 5: - return Fq(self.Q, pow(int(self.value), (self.Q + 3) // 8, self.Q)) - - # p % 8 == 1. Tonelli Shanks algorithm for finding square root - S = 0 - q = self.Q - 1 - - while q % 2 == 0: - q = q // 2 - S += 1 - - z = 0 - for i in range(self.Q): - euler = pow(i, (self.Q - 1) // 2, self.Q) - if euler == -1 % self.Q: - z = i - break - - M = S - c = pow(z, q, self.Q) - t = pow(self.value, q, self.Q) - R = pow(self.value, (q + 1) // 2, self.Q) - - while True: - if t == 0: - return Fq(self.Q, 0) - if t == 1: - return Fq(self.Q, R) - i = 0 - f = t - while f != 1: - f = pow(f, 2, self.Q) - i += 1 - b = pow(c, pow(2, M - i - 1, self.Q), self.Q) - M = i - c = pow(b, 2, self.Q) - t = (t * c) % self.Q - R = (R * b) % self.Q - - def __deepcopy__(self, memo) -> Fq: - return Fq(self.Q, self.value) - - @classmethod - def zero(cls, Q: int) -> Fq: - return Fq(Q, 0) - - @classmethod - def one(cls, Q: int) -> Fq: - return Fq(Q, 1) - - @classmethod - def from_fq(cls, Q: int, fq: Fq) -> Fq: - return fq - - -class FieldExtBase(tuple): - """ - Represents an extension of a field (or extension of an extension). - The elements of the tuple can be other FieldExtBase or they can be - Fq elements. For example, Fq2 = (Fq, Fq). Fq12 = (Fq6, Fq6), etc. - """ - - root = None - extension: int - embedding: int - basefield: Any - Q: int - - def __new__(cls, Q, *args): - new_args = args[:] - try: - arg_extension = args[0].extension - args[1].extension - except AttributeError: - if len(args) != 2: - raise Exception("Invalid number of arguments") - arg_extension = 1 - new_args = [Fq(Q, a) for a in args] - if arg_extension != 1: - if len(args) != cls.embedding: - raise Exception("Invalid number of arguments") - for arg in new_args: - assert arg.extension == arg_extension - assert all(isinstance(arg, cls.basefield) for arg in new_args) - ret = super().__new__(cls, new_args) - ret.Q = Q - return ret - - def __neg__(self): - cls = type(self) - ret = super().__new__(cls, (-x for x in self)) - ret.Q = self.Q - ret.root = self.root - return ret - - def __add__(self, other): - cls = type(self) - if not isinstance(other, cls): - if type(other) != int and other.extension > self.extension: - return NotImplemented - other_new = [cls.basefield.zero(self.Q) for _ in self] - other_new[0] = other_new[0] + other - else: - other_new = other - - ret = super().__new__(cls, (a + b for a, b in zip(self, other_new))) - ret.Q = self.Q - ret.root = self.root - return ret - - def __radd__(self, other): - return self.__add__(other) - - def __sub__(self, other): - return self + (-other) - - def __rsub__(self, other): - return (-self) + other - - def __mul__(self, other): - cls = type(self) - if isinstance(other, int): - ret = super().__new__(cls, (a * other for a in self)) - ret.Q = self.Q - ret.root = self.root - return ret - if cls.extension < other.extension: - return NotImplemented - - buf = [cls.basefield.zero(self.Q) for _ in self] - - for i, x in enumerate(self): - if cls.extension == other.extension: - for j, y in enumerate(other): - if x and y: - if i + j >= self.embedding: - buf[(i + j) % self.embedding] += x * y * self.root - else: - buf[(i + j) % self.embedding] += x * y - else: - if x: - buf[i] = x * other - ret = super().__new__(cls, buf) - ret.Q = self.Q - ret.root = self.root - return ret - - def __rmul__(self, other): - return self.__mul__(other) - - def __floordiv__(self, other): - return self * ~other - - def __eq__(self, other): - if not isinstance(other, type(self)): - if isinstance(other, FieldExtBase) or isinstance(other, int): - if ( - not isinstance(other, FieldExtBase) - or self.extension > other.extension - ): - for i in range(1, self.embedding): - if self[i] != (type(self.root).zero(self.Q)): - return False - return self[0] == other - return NotImplemented - return NotImplemented - else: - return super().__eq__(other) and self.Q == other.Q - - def __lt__(self, other): - # Reverse the order for comparison (3i + 1 > 2i + 7) - return self[::-1].__lt__(other[::-1]) - - def __gt__(self, other): - return super().__gt__(other) - - def __neq__(self, other): - return not self.__eq__(other) - - def __str__(self): - return ( - "Fq" - + str(self.extension) - + "(" - + ", ".join([a.__str__() for a in self]) - + ")" - ) - - def __repr__(self): - return ( - "Fq" - + str(self.extension) - + "(" - + ", ".join([a.__repr__() for a in self]) - + ")" - ) - - # Returns the concatenated coordinates in big endian bytes - def __bytes__(self): - sum_bytes = bytes([]) - for x in reversed(self): - if type(x) != FieldExtBase and type(x) != Fq: - x = Fq.from_fq(self.Q, x) - sum_bytes += bytes(x) - return sum_bytes - - @classmethod - def from_bytes(cls, buffer: bytes, Q: int): - assert len(buffer) == cls.extension * 48 - embedded_size = 48 * (cls.extension // cls.embedding) - tup = [] - for i in range(cls.embedding): - tup.append(buffer[i * embedded_size : (i + 1) * embedded_size]) - return cls(Q, *[cls.basefield.from_bytes(b, Q) for b in reversed(tup)]) - - __truediv__ = __floordiv__ - - def __pow__(self, e): - assert isinstance(e, int) and e >= 0 - ans = type(self).one(self.Q) - base = self - ans.root = self.root - - while e: - if e & 1: - ans *= base - - base *= base - e >>= 1 - - return ans - - def __bool__(self): - return any(x for x in self) - - def set_root(self, _root): - self.root = _root - - @classmethod - def zero(cls, Q): - return cls.from_fq(Q, Fq(Q, 0)) - - @classmethod - def one(cls, Q): - return cls.from_fq(Q, Fq(Q, 1)) - - @classmethod - def from_fq(cls, Q, fq): - y = cls.basefield.from_fq(Q, fq) - z = cls.basefield.zero(Q) - ret = super().__new__(cls, (z if i else y for i in range(cls.embedding))) - ret.Q = Q - if cls == Fq2: - ret.set_root(Fq(Q, -1)) - elif cls == Fq6: - ret.set_root(Fq2(Q, Fq.one(Q), Fq.one(Q))) - elif cls == Fq12: - r = Fq6(Q, Fq2.zero(Q), Fq2.one(Q), Fq2.zero(Q)) - ret.set_root(r) - return ret - - def __deepcopy__(self, memo): - cls = type(self) - ret = super().__new__(cls, (deepcopy(a, memo) for a in self)) - ret.Q = self.Q - ret.root = self.root - return ret - - def qi_power(self, i): - if self.Q != bls12381_q: - raise NotImplementedError - cls = type(self) - i %= cls.extension - if i == 0: - return self - ret = super().__new__( - cls, - ( - a.qi_power(i) * frob_coeffs[cls.extension, i, j] if j else a.qi_power(i) - for j, a in enumerate(self) - ), - ) - ret.Q = self.Q - ret.root = self.root - return ret - - -class Fq2(FieldExtBase): - # Fq2 is constructed as Fq(u) / (u2 - β) where β = -1 - extension = 2 - embedding = 2 - basefield = Fq - - def __init__(self, Q, *args): - super().set_root(Fq(Q, -1)) - - def __invert__(self) -> Fq2: - a, b = self - factor = ~(a * a + b * b) - ret = Fq2(self.Q, a * factor, -b * factor) - return ret - - def mul_by_nonresidue(self) -> Fq2: - # multiply by u + 1 - a, b = self - return Fq2(self.Q, a - b, a + b) - - def modsqrt(self) -> Fq2: - """ - Using algorithm 8 (complex method) for square roots in - https://eprint.iacr.org/2012/685.pdf - This is necessary for computing y value given an x value. - """ - a0, a1 = self - if a1 == Fq.zero(self.Q): - return a0.modsqrt() - alpha = pow(a0, 2) + pow(a1, 2) - gamma = pow(alpha, (self.Q - 1) // 2) - if gamma == Fq(self.Q, -1): - raise ValueError("No sqrt exists") - alpha = alpha.modsqrt() - delta = (a0 + alpha) * ~Fq(self.Q, 2) - gamma = pow(delta, (self.Q - 1) // 2) - if gamma == Fq(self.Q, -1): - delta = (a0 - alpha) * ~Fq(self.Q, 2) - - x0 = delta.modsqrt() - x1 = a1 * ~(Fq(self.Q, 2) * x0) - return Fq2(self.Q, x0, x1) - - -class Fq6(FieldExtBase): - # Fq6 is constructed as Fq2(v) / (v3 - ξ) where ξ = u + 1 - extension = 6 - embedding = 3 - basefield = Fq2 - - def __init__(self, Q: int, *args): - super().set_root(Fq2(Q, Fq.one(Q), Fq.one(Q))) - - def __invert__(self) -> Fq6: - a, b, c = self - g0 = a * a - b * c.mul_by_nonresidue() - g1 = (c * c).mul_by_nonresidue() - a * b - g2 = b * b - a * c - factor = ~(g0 * a + (g1 * c + g2 * b).mul_by_nonresidue()) - # TODO: no inverse - - return Fq6(self.Q, g0 * factor, g1 * factor, g2 * factor) - - def mul_by_nonresidue(self) -> Fq6: - # multiply by v - a, b, c = self - return Fq6(self.Q, c * self.root, a, b) - - -class Fq12(FieldExtBase): - # Fq12 is constructed as Fq6(w) / (w2 - γ) where γ = v - extension = 12 - embedding = 2 - basefield = Fq6 - - def __init__(self, Q, *args): - super().set_root(Fq6(Q, Fq2.zero(Q), Fq2.one(Q), Fq2.zero(Q))) - - def __invert__(self) -> Fq12: - a, b = self - factor = ~(a * a - (b * b).mul_by_nonresidue()) - return Fq12(self.Q, a * factor, -b * factor) - - -# Because fields aren't done with metaclasses, and we need to -# avoid circular imports, we put a hack here for bls12381 for now. -bls12381_q = ( - q -) = 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAAAB - -# roots of unity, used for computing square roots in Fq2 -rv1 = 0x6AF0E0437FF400B6831E36D6BD17FFE48395DABC2D3435E77F76E17009241C5EE67992F72EC05F4C81084FBEDE3CC09 -roots_of_unity = (Fq2(q, 1, 0), Fq2(q, 0, 1), Fq2(q, rv1, rv1), Fq2(q, rv1, q - rv1)) -del rv1 - -# Frobenius coefficients for raising elements to q**i -th powers -# These are specific to this given q -frob_coeffs = { - (2, 1, 1): Fq(q, -1), - (6, 1, 1): Fq2( - q, - Fq(q, 0x0), - Fq( - q, - 0x1A0111EA397FE699EC02408663D4DE85AA0D857D89759AD4897D29650FB85F9B409427EB4F49FFFD8BFD00000000AAAC, - ), - ), # noga: E501 - (6, 1, 2): Fq2( - q, - Fq( - q, - 0x1A0111EA397FE699EC02408663D4DE85AA0D857D89759AD4897D29650FB85F9B409427EB4F49FFFD8BFD00000000AAAD, - ), - Fq(q, 0x0), - ), # noga: E501 - (6, 2, 1): Fq2( - q, - Fq( - q, - 0x5F19672FDF76CE51BA69C6076A0F77EADDB3A93BE6F89688DE17D813620A00022E01FFFFFFFEFFFE, - ), - Fq(q, 0x0), - ), # noga: E501 - (6, 2, 2): Fq2( - q, - Fq( - q, - 0x1A0111EA397FE699EC02408663D4DE85AA0D857D89759AD4897D29650FB85F9B409427EB4F49FFFD8BFD00000000AAAC, - ), - Fq(q, 0x0), - ), - (6, 3, 1): Fq2(q, Fq(q, 0x0), Fq(q, 0x1)), - (6, 3, 2): Fq2( - q, - Fq( - q, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAAAA, - ), - Fq(q, 0x0), - ), - (6, 4, 1): Fq2( - q, - Fq( - q, - 0x1A0111EA397FE699EC02408663D4DE85AA0D857D89759AD4897D29650FB85F9B409427EB4F49FFFD8BFD00000000AAAC, - ), - Fq(q, 0x0), - ), - (6, 4, 2): Fq2( - q, - Fq( - q, - 0x5F19672FDF76CE51BA69C6076A0F77EADDB3A93BE6F89688DE17D813620A00022E01FFFFFFFEFFFE, - ), - Fq(q, 0x0), - ), - (6, 5, 1): Fq2( - q, - Fq(q, 0x0), - Fq( - q, - 0x5F19672FDF76CE51BA69C6076A0F77EADDB3A93BE6F89688DE17D813620A00022E01FFFFFFFEFFFE, - ), - ), - (6, 5, 2): Fq2( - q, - Fq( - q, - 0x5F19672FDF76CE51BA69C6076A0F77EADDB3A93BE6F89688DE17D813620A00022E01FFFFFFFEFFFF, - ), - Fq(q, 0x0), - ), - (12, 1, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x1904D3BF02BB0667C231BEB4202C0D1F0FD603FD3CBD5F4F7B2443D784BAB9C4F67EA53D63E7813D8D0775ED92235FB8, - ), - Fq( - q, - 0xFC3E2B36C4E03288E9E902231F9FB854A14787B6C7B36FEC0C8EC971F63C5F282D5AC14D6C7EC22CF78A126DDC4AF3, - ), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 2, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x5F19672FDF76CE51BA69C6076A0F77EADDB3A93BE6F89688DE17D813620A00022E01FFFFFFFEFFFF, - ), - Fq(q, 0x0), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 3, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x135203E60180A68EE2E9C448D77A2CD91C3DEDD930B1CF60EF396489F61EB45E304466CF3E67FA0AF1EE7B04121BDEA2, - ), - Fq( - q, - 0x6AF0E0437FF400B6831E36D6BD17FFE48395DABC2D3435E77F76E17009241C5EE67992F72EC05F4C81084FBEDE3CC09, - ), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 4, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x5F19672FDF76CE51BA69C6076A0F77EADDB3A93BE6F89688DE17D813620A00022E01FFFFFFFEFFFE, - ), - Fq(q, 0x0), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 5, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x144E4211384586C16BD3AD4AFA99CC9170DF3560E77982D0DB45F3536814F0BD5871C1908BD478CD1EE605167FF82995, - ), - Fq( - q, - 0x5B2CFD9013A5FD8DF47FA6B48B1E045F39816240C0B8FEE8BEADF4D8E9C0566C63A3E6E257F87329B18FAE980078116, - ), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 6, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAAAA, - ), - Fq(q, 0x0), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 7, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0xFC3E2B36C4E03288E9E902231F9FB854A14787B6C7B36FEC0C8EC971F63C5F282D5AC14D6C7EC22CF78A126DDC4AF3, - ), - Fq( - q, - 0x1904D3BF02BB0667C231BEB4202C0D1F0FD603FD3CBD5F4F7B2443D784BAB9C4F67EA53D63E7813D8D0775ED92235FB8, - ), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 8, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x1A0111EA397FE699EC02408663D4DE85AA0D857D89759AD4897D29650FB85F9B409427EB4F49FFFD8BFD00000000AAAC, - ), - Fq(q, 0x0), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 9, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x6AF0E0437FF400B6831E36D6BD17FFE48395DABC2D3435E77F76E17009241C5EE67992F72EC05F4C81084FBEDE3CC09, - ), - Fq( - q, - 0x135203E60180A68EE2E9C448D77A2CD91C3DEDD930B1CF60EF396489F61EB45E304466CF3E67FA0AF1EE7B04121BDEA2, - ), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 10, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x1A0111EA397FE699EC02408663D4DE85AA0D857D89759AD4897D29650FB85F9B409427EB4F49FFFD8BFD00000000AAAD, - ), - Fq(q, 0x0), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), - (12, 11, 1): Fq6( - q, - Fq2( - q, - Fq( - q, - 0x5B2CFD9013A5FD8DF47FA6B48B1E045F39816240C0B8FEE8BEADF4D8E9C0566C63A3E6E257F87329B18FAE980078116, - ), - Fq( - q, - 0x144E4211384586C16BD3AD4AFA99CC9170DF3560E77982D0DB45F3536814F0BD5871C1908BD478CD1EE605167FF82995, - ), - ), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - Fq2(q, Fq(q, 0x0), Fq(q, 0x0)), - ), -} - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/hash_to_field.py b/python-impl/hash_to_field.py deleted file mode 100644 index eb417219e..000000000 --- a/python-impl/hash_to_field.py +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/python -# -# pure Python implementation of hash-to-field as specified in -# https://github.com/pairingwg/bls_standard/blob/master/minutes/spec-v1.md - -import hashlib - -from bls12381 import q - - -# defined in RFC 3447, section 4.1 -def I2OSP(val, length): - if val < 0 or val >= (1 << (8 * length)): - raise ValueError("bad I2OSP call: val=%d length=%d" % (val, length)) - ret = [0] * length - val_ = val - for idx in reversed(range(0, length)): - ret[idx] = val_ & 0xFF - val_ = val_ >> 8 - ret = bytes(ret) - assert ret == int(val).to_bytes(length, "big"), "oops: %s %s" % ( - str(ret), - str(int(val).to_bytes(length, "big")), - ) - return ret - - -# defined in RFC 3447, section 4.2 -def OS2IP(octets): - ret = 0 - for o in octets: - ret = ret << 8 - ret += o - assert ret == int.from_bytes(octets, "big") - return ret - - -# expand_message_xmd from draft-irtf-cfrg-hash-to-curve-06 -def _strxor(str1, str2): - return bytes(s1 ^ s2 for (s1, s2) in zip(str1, str2)) - - -def expand_message_xmd(msg, DST, len_in_bytes, hash_fn): - # input and output lengths for hash_fn - b_in_bytes = hash_fn().digest_size - r_in_bytes = hash_fn().block_size - - # ell, DST_prime, etc - ell = (len_in_bytes + b_in_bytes - 1) // b_in_bytes - if ell > 255: - raise ValueError("expand_message_xmd: ell=%d out of range" % ell) - DST_prime = DST + I2OSP(len(DST), 1) - Z_pad = I2OSP(0, r_in_bytes) - l_i_b_str = I2OSP(len_in_bytes, 2) - - b_0 = hash_fn(Z_pad + msg + l_i_b_str + I2OSP(0, 1) + DST_prime).digest() - b_vals = [None] * ell - b_vals[0] = hash_fn(b_0 + I2OSP(1, 1) + DST_prime).digest() - for idx in range(1, ell): - b_vals[idx] = hash_fn( - _strxor(b_0, b_vals[idx - 1]) + I2OSP(idx + 1, 1) + DST_prime - ).digest() - pseudo_random_bytes = b"".join(b_vals) - return pseudo_random_bytes[0:len_in_bytes] - - -def expand_message_xof(msg, DST, len_in_bytes, hash_fn): - DST_prime = DST + I2OSP(len(DST), 1) - msg_prime = msg + I2OSP(len_in_bytes, 2) + DST_prime - return hash_fn(msg_prime).digest(len_in_bytes) - - -# hash_to_field from draft-irtf-cfrg-hash-to-curve-06 -def hash_to_field(msg, count, DST, modulus, degree, blen, expand_fn, hash_fn): - # get pseudorandom bytes - len_in_bytes = count * degree * blen - pseudo_random_bytes = expand_fn(msg, DST, len_in_bytes, hash_fn) - - u_vals = [None] * count - for idx in range(0, count): - e_vals = [None] * degree - for jdx in range(0, degree): - elm_offset = blen * (jdx + idx * degree) - tv = pseudo_random_bytes[elm_offset : elm_offset + blen] - e_vals[jdx] = OS2IP(tv) % modulus - u_vals[idx] = e_vals - return u_vals - - -def Hp(msg, count, dst): - if not isinstance(msg, bytes): - raise ValueError("Hp can't hash anything but bytes") - return hash_to_field(msg, count, dst, q, 1, 64, expand_message_xmd, hashlib.sha256) - - -def Hp2(msg, count, dst): - if not isinstance(msg, bytes): - raise ValueError("Hp2 can't hash anything but bytes") - return hash_to_field(msg, count, dst, q, 2, 64, expand_message_xmd, hashlib.sha256) diff --git a/python-impl/hd_keys.py b/python-impl/hd_keys.py deleted file mode 100644 index 1acdb311e..000000000 --- a/python-impl/hd_keys.py +++ /dev/null @@ -1,90 +0,0 @@ -from ec import G1Generator, G2Generator, JacobianPoint, default_ec -from hkdf import extract_expand -from private_key import PrivateKey -from util import hash256 - - -def key_gen(seed: bytes) -> PrivateKey: - # KeyGen - # 1. PRK = HKDF-Extract("BLS-SIG-KEYGEN-SALT-", IKM || I2OSP(0, 1)) - # 2. OKM = HKDF-Expand(PRK, keyInfo || I2OSP(L, 2), L) - # 3. SK = OS2IP(OKM) mod r - # 4. return SK - - L = 48 - # `ceil((3 * ceil(log2(r))) / 16)`, where `r` is the order of the BLS 12-381 curve - okm = extract_expand(L, seed + bytes([0]), b"BLS-SIG-KEYGEN-SALT-", bytes([0, L])) - return PrivateKey(int.from_bytes(okm, "big") % default_ec.n) - - -def ikm_to_lamport_sk(ikm: bytes, salt: bytes) -> bytes: - return extract_expand(32 * 255, ikm, salt, b"") - - -def parent_sk_to_lamport_pk(parent_sk: PrivateKey, index: int) -> bytes: - salt = index.to_bytes(4, "big") - ikm = bytes(parent_sk) - not_ikm = bytes([e ^ 0xFF for e in ikm]) # Flip bits - lamport0 = ikm_to_lamport_sk(ikm, salt) - lamport1 = ikm_to_lamport_sk(not_ikm, salt) - - lamport_pk = bytes() - for i in range(255): - lamport_pk += hash256(lamport0[i * 32 : (i + 1) * 32]) - for i in range(255): - lamport_pk += hash256(lamport1[i * 32 : (i + 1) * 32]) - - return hash256(lamport_pk) - - -def derive_child_sk(parent_sk: PrivateKey, index: int) -> PrivateKey: - """ - Derives a hardened EIP-2333 child private key, from a parent private key, - at the specified index. - """ - lamport_pk = parent_sk_to_lamport_pk(parent_sk, index) - return key_gen(lamport_pk) - - -def derive_child_sk_unhardened(parent_sk: PrivateKey, index: int) -> PrivateKey: - """ - Derives an unhardened BIP-32 child private key, from a parent private key, - at the specified index. WARNING: this key is not as secure as a hardened key. - """ - h = hash256(bytes(parent_sk.get_g1()) + index.to_bytes(4, "big")) - return PrivateKey.aggregate([PrivateKey.from_bytes(h), parent_sk]) - - -def derive_child_g1_unhardened(parent_pk: JacobianPoint, index: int) -> JacobianPoint: - """ - Derives an unhardened BIP-32 child public key, from a parent public key, - at the specified index. WARNING: this key is not as secure as a hardened key. - """ - h = hash256(bytes(parent_pk) + index.to_bytes(4, "big")) - return parent_pk + PrivateKey.from_bytes(h).value * G1Generator() - - -def derive_child_g2_unhardened(parent_pk: JacobianPoint, index: int) -> JacobianPoint: - """ - Derives an unhardened BIP-32 child public key, from a parent public key, - at the specified index. WARNING: this key is not as secure as a hardened key. - """ - h = hash256(bytes(parent_pk) + index.to_bytes(4, "big")) - return parent_pk + PrivateKey.from_bytes(h) * G2Generator() - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/hkdf.py b/python-impl/hkdf.py deleted file mode 100644 index 02c21bb4f..000000000 --- a/python-impl/hkdf.py +++ /dev/null @@ -1,53 +0,0 @@ -import hashlib -import hmac -from math import ceil - -BLOCK_SIZE = 32 - - -def extract(salt: bytes, ikm: bytes) -> bytes: - h = hmac.new(salt, ikm, hashlib.sha256) - return h.digest() - - -def expand(L: int, prk: bytes, info: bytes) -> bytes: - N: int = ceil(L / BLOCK_SIZE) - bytes_written: int = 0 - okm: bytes = b"" - - for i in range(1, N + 1): - if i == 1: - h = hmac.new(prk, info + bytes([1]), hashlib.sha256) - T: bytes = h.digest() - else: - h = hmac.new(prk, T + info + bytes([i]), hashlib.sha256) - T = h.digest() - to_write = L - bytes_written - if to_write > BLOCK_SIZE: - to_write = BLOCK_SIZE - okm += T[:to_write] - bytes_written += to_write - assert bytes_written == L - return okm - - -def extract_expand(L: int, key: bytes, salt: bytes, info: bytes) -> bytes: - prk = extract(salt, key) - return expand(L, prk, info) - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/impl-test.py b/python-impl/impl-test.py deleted file mode 100644 index 0b5141075..000000000 --- a/python-impl/impl-test.py +++ /dev/null @@ -1,663 +0,0 @@ -import hashlib -from copy import deepcopy -from secrets import token_bytes - -from ec import (G1FromBytes, G1Generator, G1Infinity, G2FromBytes, G2Generator, - G2Infinity, JacobianPoint, default_ec, default_ec_twist, - sign_Fq2, twist, untwist, y_for_x) -from fields import Fq, Fq2, Fq6, Fq12 -from hash_to_field import expand_message_xmd -from hkdf import expand, extract -from op_swu_g2 import g2_map -from pairing import ate_pairing -from private_key import PrivateKey -from schemes import AugSchemeMPL, BasicSchemeMPL, PopSchemeMPL - -G1Element = JacobianPoint -G2Element = JacobianPoint - - -def test_hkdf(): - def test_one_case( - ikm_hex, salt_hex, info_hex, prk_expected_hex, okm_expected_hex, L - ): - prk = extract(bytes.fromhex(salt_hex), bytes.fromhex(ikm_hex)) - okm = expand(L, prk, bytes.fromhex(info_hex)) - assert len(bytes.fromhex(prk_expected_hex)) == 32 - assert L == len(bytes.fromhex(okm_expected_hex)) - assert prk == bytes.fromhex(prk_expected_hex) - assert okm == bytes.fromhex(okm_expected_hex) - - test_case_1 = ( - "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", - "000102030405060708090a0b0c", - "f0f1f2f3f4f5f6f7f8f9", - "077709362c2e32df0ddc3f0dc47bba6390b6c73bb50f9c3122ec844ad7c2b3e5", - "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865", - 42, - ) - test_case_2 = ( - "000102030405060708090a0b0c0d0e0f" - "101112131415161718191a1b1c1d1e1f" - "202122232425262728292a2b2c2d2e2f" - "303132333435363738393a3b3c3d3e3f" - "404142434445464748494a4b4c4d4e4f", - "606162636465666768696a6b6c6d6e6f" - "707172737475767778797a7b7c7d7e7f" - "808182838485868788898a8b8c8d8e8f" - "909192939495969798999a9b9c9d9e9f" - "a0a1a2a3a4a5a6a7a8a9aaabacadaeaf", - "b0b1b2b3b4b5b6b7b8b9babbbcbdbebf" - "c0c1c2c3c4c5c6c7c8c9cacbcccdcecf" - "d0d1d2d3d4d5d6d7d8d9dadbdcdddedf" - "e0e1e2e3e4e5e6e7e8e9eaebecedeeef" - "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", - "06a6b88c5853361a06104c9ceb35b45cef760014904671014a193f40c15fc244", - "b11e398dc80327a1c8e7f78c596a4934" - "4f012eda2d4efad8a050cc4c19afa97c" - "59045a99cac7827271cb41c65e590e09" - "da3275600c2f09b8367793a9aca3db71" - "cc30c58179ec3e87c14c01d5c1f3434f" - "1d87", - 82, - ) - test_case_3 = ( - "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", - "", - "", - "19ef24a32c717b167f33a91d6f648bdf96596776afdb6377ac434c1c293ccb04", - "8da4e775a563c18f715f802a063c5a31b8a11f5c5ee1879ec3454e5f3c738d2d9d201395faa4b61a96c8", - 42, - ) - test_case_4 = ( - "8704f9ac024139fe62511375cf9bc534c0507dcf00c41603ac935cd5943ce0b4b88599390de14e743ca2f56a73a04eae13aa3f3b969b39d8701e0d69a6f8d42f", - "53d8e19b", - "", - "eb01c9cd916653df76ffa61b6ab8a74e254ebfd9bfc43e624cc12a72b0373dee", - "8faabea85fc0c64e7ca86217cdc6dcdc88551c3244d56719e630a3521063082c46455c2fd5483811f9520a748f0099c1dfcfa52c54e1c22b5cdf70efb0f3c676", - 64, - ) - - test_one_case(*test_case_1) - test_one_case(*test_case_2) - test_one_case(*test_case_3) - test_one_case(*test_case_4) - - -def test_eip2333(): - def test_one_case(seed_hex, master_sk_hex, child_sk_hex, child_index): - master = BasicSchemeMPL.key_gen(bytes.fromhex(seed_hex)) - child = BasicSchemeMPL.derive_child_sk(master, child_index) - - assert len(bytes(master)) == 32 - assert len(bytes(child)) == 32 - assert bytes(master) == bytes.fromhex(master_sk_hex) - assert bytes(child) == bytes.fromhex(child_sk_hex) - - test_case_1 = ( - "3141592653589793238462643383279502884197169399375105820974944592", - "4ff5e145590ed7b71e577bb04032396d1619ff41cb4e350053ed2dce8d1efd1c", - "5c62dcf9654481292aafa3348f1d1b0017bbfb44d6881d26d2b17836b38f204d", - 3141592653, - ) - test_case_2 = ( - "0099FF991111002299DD7744EE3355BBDD8844115566CC55663355668888CC00", - "1ebd704b86732c3f05f30563dee6189838e73998ebc9c209ccff422adee10c4b", - "1b98db8b24296038eae3f64c25d693a269ef1e4d7ae0f691c572a46cf3c0913c", - 4294967295, - ) - test_case_3 = ( - "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3", - "614d21b10c0e4996ac0608e0e7452d5720d95d20fe03c59a3321000a42432e1a", - "08de7136e4afc56ae3ec03b20517d9c1232705a747f588fd17832f36ae337526", - 42, - ) - test_case_intermediate = ( - "c55257c360c07c72029aebc1b53c05ed0362ada38ead3e3e9efa3708e53495531f09a6987599d18264c1e1c92f2cf141630c7a3c4ab7c81b2f001698e7463b04", - "0befcabff4a664461cc8f190cdd51c05621eb2837c71a1362df5b465a674ecfb", - "1a1de3346883401f1e3b2281be5774080edb8e5ebe6f776b0f7af9fea942553a", - 0, - ) - test_one_case(*test_case_1) - test_one_case(*test_case_2) - test_one_case(*test_case_3) - test_one_case(*test_case_intermediate) - - -def test_fields(): - a = Fq(17, 30) - b = Fq(17, -18) - c = Fq2(17, a, b) - d = Fq2(17, a + a, Fq(17, -5)) - e = c * d - f = e * d - assert f != e - e_sq = e * e - e_sqrt = e_sq.modsqrt() - assert pow(e_sqrt, 2) == e_sq - - a2 = Fq( - 172487123095712930573140951348, - 3012492130751239573498573249085723940848571098237509182375, - ) - b2 = Fq(172487123095712930573140951348, 3432984572394572309458723045723849) - c2 = Fq2(172487123095712930573140951348, a2, b2) - assert b2 != c2 - - g = Fq6(17, c, d, d * d * c) - h = Fq6(17, a + a * c, c * b * a, b * b * d * Fq(17, 21)) - i = Fq12(17, g, h) - assert ~(~i) == i - assert (~(i.root)) * i.root == Fq6.one(17) - x = Fq12(17, Fq6.zero(17), i.root) - assert (~x) * x == Fq12.one(17) - - j = Fq6(17, a + a * c, Fq2.zero(17), Fq2.zero(17)) - j2 = Fq6(17, a + a * c, Fq2.zero(17), Fq2.one(17)) - assert j == (a + a * c) - assert j2 != (a + a * c) - assert j != j2 - - # Test frob_coeffs - one = Fq(default_ec.q, 1) - two = one + one - a = Fq2(default_ec.q, two, two) - b = Fq6(default_ec.q, a, a, a) - c = Fq12(default_ec.q, b, b) - for base in (a, b, c): - for expo in range(1, base.extension): - assert base.qi_power(expo) == pow(base, pow(default_ec.q, expo)) - - -def test_ec(): - q = default_ec.q - g = G1Generator() - - assert g.is_on_curve() - assert 2 * g == g + g - assert (3 * g).is_on_curve() - assert 3 * g == g + g + g - - g2 = G2Generator() - assert g2.x * (Fq(q, 2) * g2.y) == Fq(q, 2) * (g2.x * g2.y) - assert g2.is_on_curve() - s = g2 + g2 - assert untwist(twist(s.to_affine())) == s.to_affine() - assert untwist(5 * twist(s.to_affine())) == (5 * s).to_affine() - assert 5 * twist(s.to_affine()) == twist((5 * s).to_affine()) - assert s.is_on_curve() - assert g2.is_on_curve() - assert g2 + g2 == 2 * g2 - assert g2 * 5 == (g2 * 2) + (2 * g2) + g2 - y = y_for_x(g2.x, default_ec_twist, Fq2) - assert y == g2.y or -y == g2.y - - g_j = G1Generator() - g2_j = G2Generator() - g2_j2 = G2Generator() * 2 - assert g.to_affine().to_jacobian() == g - assert (g_j * 2).to_affine() == g.to_affine() * 2 - assert (g2_j + g2_j2).to_affine() == g2.to_affine() * 3 - - -def test_edge_case_sign_Fq2(): - q = default_ec.q - a = Fq(q, 62323) - test_case_1 = Fq2(q, a, Fq(q, 0)) - test_case_2 = Fq2(q, -a, Fq(q, 0)) - assert sign_Fq2(test_case_1) != sign_Fq2(test_case_2) - - test_case_3 = Fq2(q, Fq(q, 0), a) - test_case_4 = Fq2(q, Fq(q, 0), -a) - - assert sign_Fq2(test_case_3) != sign_Fq2(test_case_4) - - -def test_xmd(): - msg = token_bytes(48) - dst = token_bytes(16) - ress = {} - for length in range(16, 8192): - result = expand_message_xmd(msg, dst, length, hashlib.sha512) - assert length == len(result) - key = result[:16] - ress[key] = ress.get(key, 0) + 1 - assert all(x == 1 for x in ress.values()) - - -def test_swu(): - dst_1 = b"QUUX-V01-CS02-with-BLS12381G2_XMD:SHA-256_SSWU_RO_" - msg_1 = b"abcdef0123456789" - res = g2_map(msg_1, dst_1).to_affine() - assert ( - res.x[0].value - == 0x121982811D2491FDE9BA7ED31EF9CA474F0E1501297F68C298E9F4C0028ADD35AEA8BB83D53C08CFC007C1E005723CD0 - ) - assert ( - res.x[1].value - == 0x190D119345B94FBD15497BCBA94ECF7DB2CBFD1E1FE7DA034D26CBBA169FB3968288B3FAFB265F9EBD380512A71C3F2C - ) - assert ( - res.y[0].value - == 0x05571A0F8D3C08D094576981F4A3B8EDA0A8E771FCDCC8ECCEAF1356A6ACF17574518ACB506E435B639353C2E14827C8 - ) - assert ( - res.y[1].value - == 0x0BB5E7572275C567462D91807DE765611490205A941A5A6AF3B1691BFE596C31225D3AABDF15FAFF860CB4EF17C7C3BE - ) - - -def test_elements(): - i1 = int.from_bytes(bytes([1, 2]), byteorder="big") - i2 = int.from_bytes(bytes([3, 1, 4, 1, 5, 9]), byteorder="big") - b1 = i1 - b2 = i2 - g1 = G1Generator() - g2 = G2Generator() - u1 = G1Infinity() - u2 = G2Infinity() - - x1 = g1 * b1 - x2 = g1 * b2 - y1 = g2 * b1 - y2 = g2 * b2 - - # G1 - assert x1 != x2 - assert x1 * b1 == b1 * x1 - assert x1 * b1 != x1 * b2 - - left = x1 + u1 - right = x1 - - assert left == right - assert x1 + x2 == x2 + x1 - assert x1 + x1.negate() == u1 - assert x1 == G1FromBytes(bytes(x1)) - copy = deepcopy(x1) - assert x1 == copy - x1 += x2 - assert x1 != copy - - # G2 - assert y1 != y2 - assert y1 * b1 == b1 * y1 - assert y1 * b1 != y1 * b2 - assert y1 + u2 == y1 - assert y1 + y2 == y2 + y1 - assert y1 + y1.negate() == u2 - assert y1 == G2FromBytes(bytes(y1)) - copy = deepcopy(y1) - assert y1 == copy - y1 += y2 - assert y1 != copy - - # pairing operation - pair = ate_pairing(x1, y1) - assert pair != ate_pairing(x1, y2) - assert pair != ate_pairing(x2, y1) - copy = deepcopy(pair) - assert pair == copy - pair = None - assert pair != copy - - sk = 728934712938472938472398074 - pk = sk * g1 - Hm = y2 * 12371928312 + y2 * 12903812903891023 - - sig = Hm * sk - - assert ate_pairing(g1, sig) == ate_pairing(pk, Hm) - - -def test_chia_vectors_1(): - seed1: bytes = bytes([0x00] * 32) - seed2: bytes = bytes([0x01] * 32) - msg1: bytes = bytes([7, 8, 9]) - msg2: bytes = bytes([10, 11, 12]) - sk1 = BasicSchemeMPL.key_gen(seed1) - sk2 = BasicSchemeMPL.key_gen(seed2) - assert ( - bytes(sk1).hex() - == "4a353be3dac091a0a7e640620372f5e1e2e4401717c1e79cac6ffba8f6905604" - ) - assert ( - bytes(sk1.get_g1()).hex() - == "85695fcbc06cc4c4c9451f4dce21cbf8de3e5a13bf48f44cdbb18e2038ba7b8bb1632d7911ef1e2e08749bddbf165352" - ) - - sig1 = BasicSchemeMPL.sign(sk1, msg1) - sig2 = BasicSchemeMPL.sign(sk2, msg2) - - assert ( - bytes(sig1).hex() - == "b8faa6d6a3881c9fdbad803b170d70ca5cbf1e6ba5a586262df368c75acd1d1ffa3ab6ee21c71f844494659878f5eb230c958dd576b08b8564aad2ee0992e85a1e565f299cd53a285de729937f70dc176a1f01432129bb2b94d3d5031f8065a1" - ) - assert bytes(sig2).hex() == ( - "a9c4d3e689b82c7ec7e838dac2380cb014f9a08f6cd6ba044c263746e39a8f7a60ffee4afb7" - "8f146c2e421360784d58f0029491e3bd8ab84f0011d258471ba4e87059de295d9aba845c044e" - "e83f6cf2411efd379ef38bf4cf41d5f3c0ae1205d" - ) - - agg_sig_1 = BasicSchemeMPL.aggregate([sig1, sig2]) - - assert bytes(agg_sig_1).hex() == ( - "aee003c8cdaf3531b6b0ca354031b0819f7586b5846796615aee8108fec75ef838d181f9d24" - "4a94d195d7b0231d4afcf06f27f0cc4d3c72162545c240de7d5034a7ef3a2a03c0159de982fb" - "c2e7790aeb455e27beae91d64e077c70b5506dea3" - ) - - assert BasicSchemeMPL.aggregate_verify( - [sk1.get_g1(), sk2.get_g1()], [msg1, msg2], agg_sig_1 - ) - - msg3: bytes = bytes([1, 2, 3]) - msg4: bytes = bytes([1, 2, 3, 4]) - msg5: bytes = bytes([1, 2]) - - sig3 = BasicSchemeMPL.sign(sk1, msg3) - sig4 = BasicSchemeMPL.sign(sk1, msg4) - sig5 = BasicSchemeMPL.sign(sk2, msg5) - - agg_sig_2 = BasicSchemeMPL.aggregate([sig3, sig4, sig5]) - assert BasicSchemeMPL.aggregate_verify( - [sk1.get_g1(), sk1.get_g1(), sk2.get_g1()], [msg3, msg4, msg5], agg_sig_2 - ) - - assert bytes(agg_sig_2).hex() == ( - "a0b1378d518bea4d1100adbc7bdbc4ff64f2c219ed6395cd36fe5d2aa44a4b8e710b607afd9" - "65e505a5ac3283291b75413d09478ab4b5cfbafbeea366de2d0c0bcf61deddaa521f6020460f" - "d547ab37659ae207968b545727beba0a3c5572b9c" - ) - - -def test_chia_vectors_2(): - msg1 = bytes([1, 2, 3, 40]) - msg2 = bytes([5, 6, 70, 201]) - msg3 = bytes([9, 10, 11, 12, 13]) - msg4 = bytes([15, 63, 244, 92, 0, 1]) - - seed1 = bytes([0x02] * 32) - seed2 = bytes([0x03] * 32) - - sk1 = AugSchemeMPL.key_gen(seed1) - sk2 = AugSchemeMPL.key_gen(seed2) - - pk1 = sk1.get_g1() - pk2 = sk2.get_g1() - - sig1 = AugSchemeMPL.sign(sk1, msg1) - sig2 = AugSchemeMPL.sign(sk2, msg2) - sig3 = AugSchemeMPL.sign(sk2, msg1) - sig4 = AugSchemeMPL.sign(sk1, msg3) - sig5 = AugSchemeMPL.sign(sk1, msg1) - sig6 = AugSchemeMPL.sign(sk1, msg4) - - agg_sig_l = AugSchemeMPL.aggregate([sig1, sig2]) - agg_sig_r = AugSchemeMPL.aggregate([sig3, sig4, sig5]) - agg_sig = AugSchemeMPL.aggregate([agg_sig_l, agg_sig_r, sig6]) - - assert AugSchemeMPL.aggregate_verify( - [pk1, pk2, pk2, pk1, pk1, pk1], [msg1, msg2, msg1, msg3, msg1, msg4], agg_sig - ) - - assert bytes(agg_sig).hex() == ( - "a1d5360dcb418d33b29b90b912b4accde535cf0e52caf467a005dc632d9f7af44b6c4e9acd4" - "6eac218b28cdb07a3e3bc087df1cd1e3213aa4e11322a3ff3847bbba0b2fd19ddc25ca964871" - "997b9bceeab37a4c2565876da19382ea32a962200" - ) - - -def test_chia_vectors_3(): - seed1: bytes = bytes([0x04] * 32) - sk1 = PopSchemeMPL.key_gen(seed1) - proof = PopSchemeMPL.pop_prove(sk1) - assert ( - bytes(proof).hex() - == "84f709159435f0dc73b3e8bf6c78d85282d19231555a8ee3b6e2573aaf66872d9203fefa1ef" - "700e34e7c3f3fb28210100558c6871c53f1ef6055b9f06b0d1abe22ad584ad3b957f3018a8f5" - "8227c6c716b1e15791459850f2289168fa0cf9115" - ) - - -def test_pyecc_vectors(): - ref_sig1Basic = b"\x96\xba4\xfa\xc3<\x7f\x12\x9d`*\x0b\xc8\xa3\xd4?\x9a\xbc\x01N\xce\xaa\xb75\x91F\xb4\xb1P\xe5{\x80\x86Es\x8f5g\x1e\x9e\x10\xe0\xd8b\xa3\x0c\xabp\x07N\xb5\x83\x1d\x13\xe6\xa5\xb1b\xd0\x1e\xeb\xe6\x87\xd0\x16J\xdb\xd0\xa8d7\n|\"*'h\xd7pM\xa2T\xf1\xbf\x18#f[\xc26\x1f\x9d\xd8\xc0\x0e\x99" - ref_sig2Basic = b'\xa4\x02y\t2\x13\x0fvj\xf1\x1b\xa7\x16Sf\x83\xd8\xc4\xcf\xa5\x19G\xe4\xf9\x08\x1f\xed\xd6\x92\xd6\xdc\x0c\xac[\x90K\xee^\xa6\xe2Ui\xe3m{\xe4\xcaY\x06\x9a\x96\xe3K\x7fp\x07X\xb7\x16\xf9IJ\xaaY\xa9nt\xd1J;U*\x9ak\xc1)\xe7\x17\x19[\x9d`\x06\xfdm\\\xefGh\xc0"\xe0\xf71j\xbf' - ref_sigABasic = b"\x98|\xfd;\xcdb(\x02\x87\x02t\x83\xf2\x9cU$^\xd81\xf5\x1d\xd6\xbd\x99\x9ao\xf1\xa1\xf1\xf1\xf0\xb6Gw\x8b\x01g5\x9cqPUX\xa7n\x15\x8ef\x18\x1e\xe5\x12Y\x05\xa6B$k\x01\xe7\xfa^\xe5=h\xa4\xfe\x9b\xfb)\xa8\xe2f\x01\xf0\xb9\xadW}\xdd\x18\x87js1|!n\xa6\x1fC\x04\x14\xecQ\xc5" - ref_sig1Aug = b'\x81\x80\xf0,\xcbr\xe9"\xb1R\xfc\xed\xbe\x0e\x1d\x19R\x105Opp6X\xe8\xe0\x8c\xbe\xbf\x11\xd4\x97\x0e\xabj\xc3\xcc\xf7\x15\xf3\xfb\x87m\xf9\xa9yz\xbd\x0c\x1a\xf6\x1a\xae\xad\xc9,,\xfe\\\nV\xc1F\xcc\x8c?qQ\xa0s\xcf_\x16\xdf8$g$\xc4\xae\xd7?\xf3\x0e\xf5\xda\xa6\xaa\xca\xed\x1a&\xec\xaa3k' - ref_sig2Aug = b'\x99\x11\x1e\xea\xfbA-\xa6\x1eL7\xd3\xe8\x06\xc6\xfdj\xc9\xf3\x87\x0eT\xda\x92"\xbaNIH"\xc5\xb7eg1\xfazdY4\xd0KU\x9e\x92a\xb8b\x01\xbb\xeeW\x05RP\xa4Y\xa2\xda\x10\xe5\x1f\x9c\x1aiA)\x7f\xfc]\x97\nUr6\xd0\xbd\xeb|\xf8\xff\x18\x80\x0b\x08c8q\xa0\xf0\xa7\xeaB\xf4t\x80' - ref_sigAAug = b"\x8c]\x03\xf9\xda\xe7~\x19\xa5\x94Z\x06\xa2\x14\x83n\xdb\x8e\x03\xb8QR]\x84\xb9\xded@\xe6\x8f\xc0\xcas\x03\xee\xed9\r\x86<\x9bU\xa8\xcfmY\x14\n\x01\xb5\x88G\x88\x1e\xb5\xafgsMD\xb2UVF\xc6al9\xab\x88\xd2S)\x9a\xcc\x1e\xb1\xb1\x9d\xdb\x9b\xfc\xbev\xe2\x8a\xdd\xf6q\xd1\x16\xc0R\xbb\x18G" - ref_sig1Pop = b"\x95P\xfbN\x7f~\x8c\xc4\xa9\x0b\xe8V\n\xb5\xa7\x98\xb0\xb20\x00\xb6\xa5J!\x17R\x02\x10\xf9\x86\xf3\xf2\x81\xb3v\xf2Y\xc0\xb7\x80b\xd1\xeb1\x92\xb3\xd9\xbb\x04\x9fY\xec\xc1\xb0:pI\xebf^\r\xf3d\x94\xaeL\xb5\xf1\x13l\xca\xee\xfc\x99X\xcb0\xc33==C\xf0qH\xc3\x86)\x9a{\x1b\xfc\r\xc5\xcf|" - ref_sig2Pop = b"\xa6\x906\xbc\x11\xae^\xfc\xbfa\x80\xaf\xe3\x9a\xdd\xde~'s\x1e\xc4\x02W\xbf\xdc<7\xf1{\x8d\xf6\x83\x06\xa3N\xbd\x10\xe9\xe3*5%7P\xdf\\\x87\xc2\x14/\x82\x07\xe8\xd5eG\x12\xb4\xe5T\xf5\x85\xfbhF\xff8\x04\xe4)\xa9\xf8\xa1\xb4\xc5ku\xd0\x86\x9e\xd6u\x80\xd7\x89\x87\x0b\xab\xe2\xc7\xc8\xa9\xd5\x1e{*" - ref_sigAPop = b"\xa4\xeat+\xcd\xc1U>\x9c\xa4\xe5`\xbe~^ln\xfajd\xdd\xdf\x9c\xa3\xbb(T#=\x85\xa6\xaa\xc1\xb7n\xc7\xd1\x03\xdbN3\x14\x8b\x82\xaf\x99#\xdb\x05\x93Jn\xce\x9aq\x01\xcd\x8a\x9dG\xce'\x97\x80V\xb0\xf5\x90\x00!\x81\x8cEi\x8a\xfd\xd6\xcf\x8ako\x7f\xee\x1f\x0bCqoU\xe4\x13\xd4\xb8z`9" - - secret1 = bytes([1] * 32) - secret2 = bytes([x * 314159 % 256 for x in range(32)]) - sk1 = PrivateKey.from_bytes(secret1) - sk2 = PrivateKey.from_bytes(secret2) - - msg = bytes([3, 1, 4, 1, 5, 9]) - sig1Basic = BasicSchemeMPL.sign(sk1, msg) - sig2Basic = BasicSchemeMPL.sign(sk2, msg) - sigABasic = BasicSchemeMPL.aggregate([sig1Basic, sig2Basic]) - sig1Aug = AugSchemeMPL.sign(sk1, msg) - sig2Aug = AugSchemeMPL.sign(sk2, msg) - sigAAug = AugSchemeMPL.aggregate([sig1Aug, sig2Aug]) - sig1Pop = PopSchemeMPL.sign(sk1, msg) - sig2Pop = PopSchemeMPL.sign(sk2, msg) - sigAPop = PopSchemeMPL.aggregate([sig1Pop, sig2Pop]) - - assert bytes(sig1Basic) == ref_sig1Basic - print(bytes(sig1Basic).hex()) - assert bytes(sig2Basic) == ref_sig2Basic - assert bytes(sigABasic) == ref_sigABasic - assert bytes(sig1Aug) == ref_sig1Aug - assert bytes(sig2Aug) == ref_sig2Aug - assert bytes(sigAAug) == ref_sigAAug - assert bytes(sig1Pop) == ref_sig1Pop - assert bytes(sig2Pop) == ref_sig2Pop - assert bytes(sigAPop) == ref_sigAPop - - -def test_vectors_invalid(): - # Invalid inputs from https://github.com/algorand/bls_sigs_ref/blob/master/python-impl/serdesZ.py - invalid_inputs_1 = [ - # infinity points: too short - "c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # infinity points: not all zeros - "c00000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000", - # bad tags - "3a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - "7a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - "fa0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - # wrong length for compresed point - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa", - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaaaa", - # invalid x-coord - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - # invalid elm of Fp --- equal to p (must be strictly less) - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", - ] - invalid_inputs_2 = [ - # infinity points: too short - "c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # infinity points: not all zeros - "c00000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000", - # bad tags - "3a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "7a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "fa0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # wrong length for compressed point - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # invalid x-coord - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaa7", - # invalid elm of Fp --- equal to p (must be strictly less) - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", - ] - - for s in invalid_inputs_1: - bytes_ = bytes.fromhex(s) - try: - g1 = G1FromBytes(bytes_) - assert g1 is not None - assert False, "Failed to disallow creation of G1 element." - except Exception: - pass - - for s in invalid_inputs_2: - bytes_ = bytes.fromhex(s) - try: - g2 = G2FromBytes(bytes_) - assert g2 is not None - assert False, "Failed to disallow creation of G2 element." - except Exception: - pass - - -def test_readme(): - seed: bytes = bytes( - [ - 0, - 50, - 6, - 244, - 24, - 199, - 1, - 25, - 52, - 88, - 192, - 19, - 18, - 12, - 89, - 6, - 220, - 18, - 102, - 58, - 209, - 82, - 12, - 62, - 89, - 110, - 182, - 9, - 44, - 20, - 254, - 22, - ] - ) - sk: PrivateKey = AugSchemeMPL.key_gen(seed) - pk: G1Element = sk.get_g1() - - message: bytes = bytes([1, 2, 3, 4, 5]) - signature: G2Element = AugSchemeMPL.sign(sk, message) - - ok: bool = AugSchemeMPL.verify(pk, message, signature) - assert ok - - sk_bytes: bytes = bytes(sk) # 32 bytes - pk_bytes: bytes = bytes(pk) # 48 bytes - signature_bytes: bytes = bytes(signature) # 96 bytes - - print(sk_bytes.hex(), pk_bytes.hex(), signature_bytes.hex()) - - sk = PrivateKey.from_bytes(sk_bytes) - assert sk is not None - pk = G1FromBytes(pk_bytes) - assert pk is not None - signature: G2Element = G2FromBytes(signature_bytes) - - seed = bytes([1]) + seed[1:] - sk1: PrivateKey = AugSchemeMPL.key_gen(seed) - seed = bytes([2]) + seed[1:] - sk2: PrivateKey = AugSchemeMPL.key_gen(seed) - message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) - - pk1: G1Element = sk1.get_g1() - sig1: G2Element = AugSchemeMPL.sign(sk1, message) - - pk2: G1Element = sk2.get_g1() - sig2: G2Element = AugSchemeMPL.sign(sk2, message2) - - agg_sig: G2Element = AugSchemeMPL.aggregate([sig1, sig2]) - - ok = AugSchemeMPL.aggregate_verify([pk1, pk2], [message, message2], agg_sig) - assert ok - - seed = bytes([3]) + seed[1:] - sk3: PrivateKey = AugSchemeMPL.key_gen(seed) - pk3: G1Element = sk3.get_g1() - message3: bytes = bytes([100, 2, 254, 88, 90, 45, 23]) - sig3: G2Element = AugSchemeMPL.sign(sk3, message3) - - agg_sig_final: G2Element = AugSchemeMPL.aggregate([agg_sig, sig3]) - ok = AugSchemeMPL.aggregate_verify( - [pk1, pk2, pk3], [message, message2, message3], agg_sig_final - ) - assert ok - - pop_sig1: G2Element = PopSchemeMPL.sign(sk1, message) - pop_sig2: G2Element = PopSchemeMPL.sign(sk2, message) - pop_sig3: G2Element = PopSchemeMPL.sign(sk3, message) - pop1: G2Element = PopSchemeMPL.pop_prove(sk1) - pop2: G2Element = PopSchemeMPL.pop_prove(sk2) - pop3: G2Element = PopSchemeMPL.pop_prove(sk3) - - ok = PopSchemeMPL.pop_verify(pk1, pop1) - assert ok - ok = PopSchemeMPL.pop_verify(pk2, pop2) - assert ok - ok = PopSchemeMPL.pop_verify(pk3, pop3) - assert ok - - pop_sig_agg: G2Element = PopSchemeMPL.aggregate([pop_sig1, pop_sig2, pop_sig3]) - - ok = PopSchemeMPL.fast_aggregate_verify([pk1, pk2, pk3], message, pop_sig_agg) - assert ok - - pop_agg_pk: G1Element = pk1 + pk2 + pk3 - ok = PopSchemeMPL.verify(pop_agg_pk, message, pop_sig_agg) - assert ok - - pop_agg_sk: PrivateKey = PrivateKey.aggregate([sk1, sk2, sk3]) - ok = PopSchemeMPL.sign(pop_agg_sk, message) == pop_sig_agg - assert ok - - master_sk: PrivateKey = AugSchemeMPL.key_gen(seed) - child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) - grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) - assert grandchild is not None - - master_pk: G1Element = master_sk.get_g1() - child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) - grandchild_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(child_u, 0) - - child_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(master_pk, 22) - grandchild_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(child_u_pk, 0) - - ok = grandchild_u_pk == grandchild_u.get_g1() - assert ok - - -test_hkdf() -test_eip2333() -test_fields() -test_ec() -test_xmd() -test_swu() -test_edge_case_sign_Fq2() -test_elements() -test_chia_vectors_1() -test_chia_vectors_2() -test_chia_vectors_3() -test_pyecc_vectors() -test_vectors_invalid() -test_readme() diff --git a/python-impl/op_swu_g2.py b/python-impl/op_swu_g2.py deleted file mode 100644 index afdb40cf7..000000000 --- a/python-impl/op_swu_g2.py +++ /dev/null @@ -1,214 +0,0 @@ -#!/usr/bin/python -# -# pure Python implementation of optimized simplified SWU map to BLS12-381 G2 -# https://github.com/algorand/bls_sigs_ref -# -# This software is (C) 2019 Algorand, Inc. -# -# Licensed under the MIT license (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://opensource.org/licenses/MIT - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from bls12381 import h_eff, q -from ec import JacobianPoint, default_ec_twist, eval_iso -from fields import Fq, Fq2, roots_of_unity -from hash_to_field import Hp2 -from typing import Union - - -def sgn0(x: Fq2) -> int: - # https://tools.ietf.org/html/draft-irtf-cfrg-hash-to-curve-07#section-4.1 - - sign_0: int = x[0].value % 2 - zero_0: bool = x[0] == 0 - sign_1: int = x[1].value % 2 - return sign_0 or (zero_0 and sign_1) - - -# distinguished non-square in Fp2 for SWU map -xi_2 = Fq2(q, -2, -1) - -# 3-isogenous curve parameters -Ell2p_a = Fq2(q, 0, 240) -Ell2p_b = Fq2(q, 1012, 1012) - - -# eta values, used for computing sqrt(g(X1(t))) -# For details on how to compute, see ../sage-impl/opt_sswu_g2.sage -ev1 = 0x699BE3B8C6870965E5BF892AD5D2CC7B0E85A117402DFD83B7F4A947E02D978498255A2AAEC0AC627B5AFBDF1BF1C90 -ev2 = 0x8157CD83046453F5DD0972B6E3949E4288020B5B8A9CC99CA07E27089A2CE2436D965026ADAD3EF7BABA37F2183E9B5 -ev3 = 0xAB1C2FFDD6C253CA155231EB3E71BA044FD562F6F72BC5BAD5EC46A0B7A3B0247CF08CE6C6317F40EDBC653A72DEE17 -ev4 = 0xAA404866706722864480885D68AD0CCAC1967C7544B447873CC37E0181271E006DF72162A3D3E0287BF597FBF7F8FC1 -etas = (Fq2(q, ev1, ev2), Fq2(q, q - ev2, ev1), Fq2(q, ev3, ev4), Fq2(q, q - ev4, ev3)) -del ev1, ev2, ev3, ev4 - - -# -# Simplified SWU map, optimized and adapted to Ell2' -# -# This function maps an element of Fp^2 to the curve Ell2', 3-isogenous to Ell2. -def osswu2_help(t): - assert isinstance(t, Fq2) - - # first, compute X0(t), detecting and handling exceptional case - num_den_common = xi_2 ** 2 * t ** 4 + xi_2 * t ** 2 - x0_num = Ell2p_b * (num_den_common + Fq(q, 1)) - x0_den = -Ell2p_a * num_den_common - x0_den = Ell2p_a * xi_2 if x0_den == 0 else x0_den - - # compute num and den of g(X0(t)) - gx0_den = pow(x0_den, 3) - gx0_num = Ell2p_b * gx0_den - gx0_num += Ell2p_a * x0_num * pow(x0_den, 2) - gx0_num += pow(x0_num, 3) - - # try taking sqrt of g(X0(t)) - # this uses the trick for combining division and sqrt from Section 5 of - # Bernstein, Duif, Lange, Schwabe, and Yang, "High-speed high-security signatures." - # J Crypt Eng 2(2):77--89, Sept. 2012. http://ed25519.cr.yp.to/ed25519-20110926.pdf - tmp1 = pow(gx0_den, 7) # v^7 - tmp2 = gx0_num * tmp1 # u v^7 - tmp1 = tmp1 * tmp2 * gx0_den # u v^15 - sqrt_candidate = tmp2 * pow(tmp1, (q ** 2 - 9) // 16) - - # check if g(X0(t)) is square and return the sqrt if so - for root in roots_of_unity: - y0 = sqrt_candidate * root - if y0 ** 2 * gx0_den == gx0_num: - # found sqrt(g(X0(t))). force sign of y to equal sign of t - if sgn0(y0) != sgn0(t): - y0 = -y0 - assert sgn0(y0) == sgn0(t) - return JacobianPoint( - x0_num * x0_den, y0 * pow(x0_den, 3), x0_den, False, default_ec_twist - ) - - # if we've gotten here, then g(X0(t)) is not square. convert srqt_candidate to sqrt(g(X1(t))) - (x1_num, x1_den) = (xi_2 * t ** 2 * x0_num, x0_den) - (gx1_num, gx1_den) = (xi_2 ** 3 * t ** 6 * gx0_num, gx0_den) - sqrt_candidate *= t ** 3 - for eta in etas: - y1 = eta * sqrt_candidate - if y1 ** 2 * gx1_den == gx1_num: - # found sqrt(g(X1(t))). force sign of y to equal sign of t - if sgn0(y1) != sgn0(t): - y1 = -y1 - assert sgn0(y1) == sgn0(t) - return JacobianPoint( - x1_num * x1_den, y1 * pow(x1_den, 3), x1_den, False, default_ec_twist - ) - - # if we got here, something is wrong - raise RuntimeError("osswu2_help failed for unknown reasons") - - -# -# 3-Isogeny from Ell2' to Ell2 -# -# coefficients for the 3-isogeny map from Ell2' to Ell2 -xnum = ( - Fq2( - q, - 0x5C759507E8E333EBB5B7A9A47D7ED8532C52D39FD3A042A88B58423C50AE15D5C2638E343D9C71C6238AAAAAAAA97D6, - 0x5C759507E8E333EBB5B7A9A47D7ED8532C52D39FD3A042A88B58423C50AE15D5C2638E343D9C71C6238AAAAAAAA97D6, - ), - Fq2( - q, - 0x0, - 0x11560BF17BAA99BC32126FCED787C88F984F87ADF7AE0C7F9A208C6B4F20A4181472AAA9CB8D555526A9FFFFFFFFC71A, - ), - Fq2( - q, - 0x11560BF17BAA99BC32126FCED787C88F984F87ADF7AE0C7F9A208C6B4F20A4181472AAA9CB8D555526A9FFFFFFFFC71E, - 0x8AB05F8BDD54CDE190937E76BC3E447CC27C3D6FBD7063FCD104635A790520C0A395554E5C6AAAA9354FFFFFFFFE38D, - ), - Fq2( - q, - 0x171D6541FA38CCFAED6DEA691F5FB614CB14B4E7F4E810AA22D6108F142B85757098E38D0F671C7188E2AAAAAAAA5ED1, - 0x0, - ), -) -xden = ( - Fq2( - q, - 0x0, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAA63, - ), - Fq2( - q, - 0xC, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAA9F, - ), - Fq2(q, 0x1, 0x0), -) -ynum = ( - Fq2( - q, - 0x1530477C7AB4113B59A4C18B076D11930F7DA5D4A07F649BF54439D87D27E500FC8C25EBF8C92F6812CFC71C71C6D706, - 0x1530477C7AB4113B59A4C18B076D11930F7DA5D4A07F649BF54439D87D27E500FC8C25EBF8C92F6812CFC71C71C6D706, - ), - Fq2( - q, - 0x0, - 0x5C759507E8E333EBB5B7A9A47D7ED8532C52D39FD3A042A88B58423C50AE15D5C2638E343D9C71C6238AAAAAAAA97BE, - ), - Fq2( - q, - 0x11560BF17BAA99BC32126FCED787C88F984F87ADF7AE0C7F9A208C6B4F20A4181472AAA9CB8D555526A9FFFFFFFFC71C, - 0x8AB05F8BDD54CDE190937E76BC3E447CC27C3D6FBD7063FCD104635A790520C0A395554E5C6AAAA9354FFFFFFFFE38F, - ), - Fq2( - q, - 0x124C9AD43B6CF79BFBF7043DE3811AD0761B0F37A1E26286B0E977C69AA274524E79097A56DC4BD9E1B371C71C718B10, - 0x0, - ), -) -yden = ( - Fq2( - q, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFA8FB, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFA8FB, - ), - Fq2( - q, - 0x0, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFA9D3, - ), - Fq2( - q, - 0x12, - 0x1A0111EA397FE69A4B1BA7B6434BACD764774B84F38512BF6730D2A0F6B0F6241EABFFFEB153FFFFB9FEFFFFFFFFAA99, - ), - Fq2(q, 0x1, 0x0), -) - - -# compute 3-isogeny map from Ell2' to Ell2 -def iso3(P): - return eval_iso(P, (xnum, xden, ynum, yden), default_ec_twist) - - -# -# map from Fq2 element(s) to point in G2 subgroup of Ell2 -# -def opt_swu2_map(t: Fq2, t2: Union[Fq2, None] = None) -> JacobianPoint: - Pp = iso3(osswu2_help(t)) - if t2 is not None: - Pp2 = iso3(osswu2_help(t2)) - Pp = Pp + Pp2 - return Pp * h_eff - - -# -# map from bytes() to point in G2 subgroup of Ell2 -# -def g2_map(alpha: bytes, dst=None): - return opt_swu2_map(*(Fq2(q, *hh) for hh in Hp2(alpha, 2, dst))) diff --git a/python-impl/pairing.py b/python-impl/pairing.py deleted file mode 100644 index ca752311e..000000000 --- a/python-impl/pairing.py +++ /dev/null @@ -1,136 +0,0 @@ -from collections import namedtuple -from typing import List - -import bls12381 -from ec import AffinePoint, JacobianPoint, untwist -from fields import Fq, Fq12 - -# Struct for elliptic curve parameters -EC = namedtuple("EC", "q a b gx gy g2x g2y n h x k sqrt_n3 sqrt_n3m1o2") - -default_ec = EC(*bls12381.parameters()) -default_ec_twist = EC(*bls12381.parameters()) - - -def int_to_bits(i: int) -> List[int]: - if i < 1: - return [0] - bits = [] - while i != 0: - bits.append(i % 2) - i = i // 2 - return list(reversed(bits)) - - -def double_line_eval(R: AffinePoint, P: AffinePoint, ec=default_ec): - """ - Creates an equation for a line tangent to R, - and evaluates this at the point P. f(x) = y - sv - v. - f(P). - """ - R12 = untwist(R) - - slope = (Fq(ec.q, 3) * (R12.x ** 2) + ec.a) / (Fq(ec.q, 2) * R12.y) - v = R12.y - slope * R12.x - - return P.y - P.x * slope - v - - -def add_line_eval(R: AffinePoint, Q: AffinePoint, P: AffinePoint, ec=default_ec) -> Fq: - """ - Creates an equation for a line between R and Q, - and evaluates this at the point P. f(x) = y - sv - v. - f(P). - """ - R12 = untwist(R) - Q12 = untwist(Q) - - # This is the case of a vertical line, where the denominator - # will be 0. - if R12 == Q12.negate(): - return P.x - R12.x - - slope = (Q12.y - R12.y) / (Q12.x - R12.x) - v = (Q12.y * R12.x - R12.y * Q12.x) / (R12.x - Q12.x) - - return P.y - P.x * slope - v - - -def miller_loop(T: int, P: AffinePoint, Q: AffinePoint, ec=default_ec) -> Fq12: - """ - Performs a double and add algorithm for the ate pairing. This algorithm - is taken from Craig Costello's "Pairing for Beginners". - """ - T_bits = int_to_bits(T) - R = Q - f = Fq12.one(ec.q) # f is an element of Fq12 - for i in range(1, len(T_bits)): - # Compute sloped line lrr - lrr = double_line_eval(R, P, ec) - f = f * f * lrr - - R = Fq(ec.q, 2) * R - if T_bits[i] == 1: - # Compute sloped line lrq - lrq = add_line_eval(R, Q, P, ec) - f = f * lrq - - R = R + Q - return f - - -def final_exponentiation(element: Fq12, ec=default_ec) -> Fq12: - """ - Performs a final exponentiation to map the result of the Miller - loop to a unique element of Fq12. - """ - if ec.k == 12: - ans = element ** ((pow(ec.q, 4) - pow(ec.q, 2) + 1) // ec.n) - ans = ans.qi_power(2) * ans - ans = ans.qi_power(6) / ans - return ans - else: - return element ** ((pow(ec.q, ec.k) - 1) // ec.n) - - -def ate_pairing(P: JacobianPoint, Q: JacobianPoint, ec=default_ec) -> Fq12: - """ - Performs one ate pairing. - """ - t = default_ec.x + 1 - T = abs(t - 1) - element = miller_loop(T, P.to_affine(), Q.to_affine(), ec) - return final_exponentiation(element, ec) - - -def ate_pairing_multi( - Ps: List[JacobianPoint], Qs: List[JacobianPoint], ec=default_ec -) -> Fq12: - """ - Computes multiple pairings at once. This is more efficient, - since we can multiply all the results of the miller loops, - and perform just one final exponentiation. - """ - t = default_ec.x + 1 - T = abs(t - 1) - prod = Fq12.one(ec.q) - for i in range(len(Qs)): - prod *= miller_loop(T, Ps[i].to_affine(), Qs[i].to_affine(), ec) - return final_exponentiation(prod, ec) - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/private_key.py b/python-impl/private_key.py deleted file mode 100644 index 200bd6304..000000000 --- a/python-impl/private_key.py +++ /dev/null @@ -1,81 +0,0 @@ -from __future__ import annotations - -from ec import G1Generator, default_ec -from hkdf import extract_expand - - -class PrivateKey: - """ - Private keys are just random integers between 1 and the group order. - """ - - PRIVATE_KEY_SIZE = 32 - - def __init__(self, value): - assert value < default_ec.n - self.value = value - - @staticmethod - def from_bytes(buffer): - return PrivateKey(int.from_bytes(buffer, "big") % default_ec.n) - - @staticmethod - def from_seed(seed): - L = 48 - # `ceil((3 * ceil(log2(r))) / 16)`, where `r` is the order of the BLS 12-381 curve - okm = extract_expand( - L, seed + bytes([0]), b"BLS-SIG-KEYGEN-SALT-", bytes([0, L]) - ) - return PrivateKey(int.from_bytes(okm, "big") % default_ec.n) - - @staticmethod - def from_int(n: int): - return PrivateKey(n % default_ec.n) - - def get_g1(self): - return self.value * G1Generator() - - def sign(self, m): - pass - - def __eq__(self, other): - return self.value == other.value - - def __hash__(self): - return self.value - - def __bytes__(self): - return self.value.to_bytes(self.PRIVATE_KEY_SIZE, "big") - - def size(self): - return self.PRIVATE_KEY_SIZE - - def __str__(self): - return "PrivateKey(0x" + bytes(self).hex() + ")" - - def __repr__(self): - return "PrivateKey(0x" + bytes(self).hex() + ")" - - @staticmethod - def aggregate(private_keys): - """ - Aggregates private keys together - """ - return PrivateKey(sum(pk.value for pk in private_keys) % default_ec.n) - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/python-impl/schemes.py b/python-impl/schemes.py deleted file mode 100644 index 7b7a1d973..000000000 --- a/python-impl/schemes.py +++ /dev/null @@ -1,209 +0,0 @@ -from typing import List - -from ec import G1Generator, JacobianPoint, default_ec -from fields import Fq12 -from hd_keys import (derive_child_g1_unhardened, derive_child_sk, - derive_child_sk_unhardened, key_gen) -from op_swu_g2 import g2_map -from pairing import ate_pairing_multi -from private_key import PrivateKey - -basic_scheme_dst = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_" -aug_scheme_dst = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_AUG_" -pop_scheme_dst = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_" -pop_scheme_pop_dst = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_" - - -def core_sign_mpl(sk: PrivateKey, message: bytes, dst: bytes) -> JacobianPoint: - return sk.value * g2_map(message, dst) - - -def core_verify_mpl( - pk: JacobianPoint, message: bytes, signature: JacobianPoint, dst: bytes -) -> bool: - try: - signature.check_valid() - pk.check_valid() - except AssertionError: - return False - q = g2_map(message, dst) - one = Fq12.one(default_ec.q) - pairing_result = ate_pairing_multi([pk, G1Generator().negate()], [q, signature]) - return pairing_result == one - - -def core_aggregate_mpl(signatures: List[JacobianPoint]) -> JacobianPoint: - if len(signatures) < 1: - raise ValueError("Must aggregate at least 1 signature") - aggregate = signatures[0] - aggregate.check_valid() - for signature in signatures[1:]: - signature.check_valid() - aggregate += signature - return aggregate - - -def core_aggregate_verify( - pks: List[JacobianPoint], ms: List[bytes], signature: JacobianPoint, dst: bytes -) -> bool: - if len(pks) != len(ms) or len(pks) < 1: - return False - try: - signature.check_valid() - qs = [signature] - ps = [G1Generator().negate()] - for i in range(len(pks)): - pks[i].check_valid() - qs.append(g2_map(ms[i], dst)) - ps.append(pks[i]) - return Fq12.one(default_ec.q) == ate_pairing_multi(ps, qs) - - except AssertionError: - return False - - -class BasicSchemeMPL: - @staticmethod - def key_gen(seed: bytes) -> PrivateKey: - return key_gen(seed) - - @staticmethod - def sign(sk: PrivateKey, message: bytes) -> JacobianPoint: - return core_sign_mpl(sk, message, basic_scheme_dst) - - @staticmethod - def verify(pk: JacobianPoint, message: bytes, signature: JacobianPoint) -> bool: - return core_verify_mpl(pk, message, signature, basic_scheme_dst) - - @staticmethod - def aggregate(signatures: List[JacobianPoint]) -> JacobianPoint: - return core_aggregate_mpl(signatures) - - @staticmethod - def aggregate_verify( - pks: List[JacobianPoint], ms: List[bytes], signature: JacobianPoint - ) -> bool: - if len(pks) != len(ms) or len(pks) < 1: - return False - if len(set(ms)) != len(ms): - # Disallow repeated messages - return False - return core_aggregate_verify(pks, ms, signature, basic_scheme_dst) - - @staticmethod - def derive_child_sk(sk: PrivateKey, index: int) -> PrivateKey: - return derive_child_sk(sk, index) - - @staticmethod - def derive_child_sk_unhardened(sk: PrivateKey, index: int) -> PrivateKey: - return derive_child_sk_unhardened(sk, index) - - @staticmethod - def derive_child_pk_unhardened(pk: JacobianPoint, index: int) -> JacobianPoint: - return derive_child_g1_unhardened(pk, index) - - -class AugSchemeMPL: - @staticmethod - def key_gen(seed: bytes) -> PrivateKey: - return key_gen(seed) - - @staticmethod - def sign(sk: PrivateKey, message: bytes) -> JacobianPoint: - pk = sk.get_g1() - return core_sign_mpl(sk, bytes(pk) + message, aug_scheme_dst) - - @staticmethod - def verify(pk: JacobianPoint, message: bytes, signature: JacobianPoint) -> bool: - return core_verify_mpl(pk, bytes(pk) + message, signature, aug_scheme_dst) - - @staticmethod - def aggregate(signatures: List[JacobianPoint]) -> JacobianPoint: - return core_aggregate_mpl(signatures) - - @staticmethod - def aggregate_verify( - pks: List[JacobianPoint], ms: List[bytes], signature: JacobianPoint - ) -> bool: - if len(pks) != len(ms) or len(pks) < 1: - return False - m_primes = [bytes(pks[i]) + ms[i] for i in range(len(pks))] - return core_aggregate_verify(pks, m_primes, signature, aug_scheme_dst) - - @staticmethod - def derive_child_sk(sk: PrivateKey, index: int) -> PrivateKey: - return derive_child_sk(sk, index) - - @staticmethod - def derive_child_sk_unhardened(sk: PrivateKey, index: int) -> PrivateKey: - return derive_child_sk_unhardened(sk, index) - - @staticmethod - def derive_child_pk_unhardened(pk: JacobianPoint, index: int) -> JacobianPoint: - return derive_child_g1_unhardened(pk, index) - - -class PopSchemeMPL: - @staticmethod - def key_gen(seed: bytes) -> PrivateKey: - return key_gen(seed) - - @staticmethod - def sign(sk: PrivateKey, message: bytes) -> JacobianPoint: - return core_sign_mpl(sk, message, pop_scheme_dst) - - @staticmethod - def verify(pk: JacobianPoint, message: bytes, signature: JacobianPoint) -> bool: - return core_verify_mpl(pk, message, signature, pop_scheme_dst) - - @staticmethod - def aggregate(signatures: List[JacobianPoint]) -> JacobianPoint: - return core_aggregate_mpl(signatures) - - @staticmethod - def aggregate_verify( - pks: List[JacobianPoint], ms: List[bytes], signature: JacobianPoint - ) -> bool: - if len(pks) != len(ms) or len(pks) < 1: - return False - return core_aggregate_verify(pks, ms, signature, pop_scheme_dst) - - @staticmethod - def pop_prove(sk: PrivateKey) -> JacobianPoint: - pk: JacobianPoint = sk.get_g1() - return sk.value * g2_map(bytes(pk), pop_scheme_pop_dst) - - @staticmethod - def pop_verify(pk: JacobianPoint, proof: JacobianPoint) -> bool: - try: - proof.check_valid() - pk.check_valid() - q = g2_map(bytes(pk), pop_scheme_pop_dst) - one = Fq12.one(default_ec.q) - pairing_result = ate_pairing_multi([pk, G1Generator().negate()], [q, proof]) - return pairing_result == one - except AssertionError: - return False - - @staticmethod - def fast_aggregate_verify( - pks: List[JacobianPoint], message: bytes, signature: JacobianPoint - ) -> bool: - if len(pks) < 1: - return False - aggregate: JacobianPoint = pks[0] - for pk in pks[1:]: - aggregate += pk - return core_verify_mpl(aggregate, message, signature, pop_scheme_dst) - - @staticmethod - def derive_child_sk(sk: PrivateKey, index: int) -> PrivateKey: - return derive_child_sk(sk, index) - - @staticmethod - def derive_child_sk_unhardened(sk: PrivateKey, index: int) -> PrivateKey: - return derive_child_sk_unhardened(sk, index) - - @staticmethod - def derive_child_pk_unhardened(pk: JacobianPoint, index: int) -> JacobianPoint: - return derive_child_g1_unhardened(pk, index) diff --git a/python-impl/util.py b/python-impl/util.py deleted file mode 100644 index 09192fd68..000000000 --- a/python-impl/util.py +++ /dev/null @@ -1,49 +0,0 @@ -import hashlib - -HMAC_BLOCK_SIZE = 64 - - -def hash256(m): - if type(m) != bytes: - m = m.encode("utf-8") - return hashlib.sha256(m).digest() - - -def hash512(m): - if type(m) != bytes: - m = m.encode("utf-8") - return hash256(m + bytes([0])) + hash256(m + bytes([1])) - - -def hmac256(m, k): - if type(m) != bytes and type(m) != bytearray: - m = m.encode("utf-8") - if type(k) != bytes and type(k) != bytearray: - k = k.encode("utf-8") - k = bytes(k) - if len(k) > HMAC_BLOCK_SIZE: - k = hash256(k) - while len(k) < HMAC_BLOCK_SIZE: - k += bytes([0]) - opad = bytes([0x5C] * HMAC_BLOCK_SIZE) - ipad = bytes([0x36] * HMAC_BLOCK_SIZE) - kopad = bytes([k[i] ^ opad[i] for i in range(HMAC_BLOCK_SIZE)]) - kipad = bytes([k[i] ^ ipad[i] for i in range(HMAC_BLOCK_SIZE)]) - return hash256(kopad + hash256(kipad + m)) - - -""" -Copyright 2020 Chia Network Inc - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" From 34b76e992685ad344ada5e77963df83f52921486 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 13:02:57 +0530 Subject: [PATCH 04/31] chore: drop unused and unmaintained workflows --- .github/workflows/build-wheels.yml | 357 ---------------------------- .github/workflows/js-bindings.yml | 60 ----- .github/workflows/relic-nightly.yml | 78 ------ 3 files changed, 495 deletions(-) delete mode 100644 .github/workflows/build-wheels.yml delete mode 100644 .github/workflows/js-bindings.yml delete mode 100644 .github/workflows/relic-nightly.yml diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml deleted file mode 100644 index 6953beebc..000000000 --- a/.github/workflows/build-wheels.yml +++ /dev/null @@ -1,357 +0,0 @@ -name: build - check - upload - -on: - push: - branches: - - main - tags: - - '**' - pull_request: - branches: - - '**' - -concurrency: - # SHA is added to the end if on `main` to let all main workflows run - group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || startsWith(github.ref, 'refs/heads/long_lived/')) && github.sha || '' }} - cancel-in-progress: true - -jobs: - build-wheels: - name: Wheel - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} - runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} - strategy: - fail-fast: false - matrix: - os: - - name: macOS - matrix: macos - runs-on: - arm: [macOS, ARM64] - intel: [macos-latest] - cibw-archs-macos: - arm: arm64 - intel: x86_64 - - name: Ubuntu - matrix: ubuntu - runs-on: - arm: [Linux, ARM64] - intel: [ubuntu-latest] - - name: Windows - matrix: windows - runs-on: - intel: [windows-latest] - python: - - major-dot-minor: '3.7' - cibw-build: 'cp37-*' - manylinux: - arch: manylinux2014 - intel: manylinux2010 - matrix: '3.7' - - major-dot-minor: '3.8' - cibw-build: 'cp38-*' - manylinux: - arch: manylinux2014 - intel: manylinux2010 - matrix: '3.8' - - major-dot-minor: '3.9' - cibw-build: 'cp39-*' - manylinux: - arch: manylinux2014 - intel: manylinux2010 - matrix: '3.9' - - major-dot-minor: '3.10' - cibw-build: 'cp310-*' - manylinux: - arch: manylinux2014 - intel: manylinux2010 - matrix: '3.10' - - major-dot-minor: '3.11' - cibw-build: 'cp311-*' - manylinux: - arch: manylinux2014 - intel: manylinux2014 - matrix: '3.11' - arch: - - name: ARM - matrix: arm - - name: Intel - matrix: intel - exclude: - # Only partial entries are required here by GitHub Actions so generally I - # only specify the `matrix:` entry. The super linter complains so for now - # all entries are included to avoid that. Reported at - # https://github.com/github/super-linter/issues/3016 - - os: - name: Windows - matrix: windows - runs-on: - intel: [windows-latest] - arch: - name: ARM - matrix: arm - - os: - name: macOS - matrix: macos - runs-on: - arm: [macOS, ARM64] - intel: [macos-latest] - python: - major-dot-minor: '3.7' - cibw-build: 'cp37-*' - matrix: '3.7' - arch: - name: ARM - matrix: arm - - os: - name: macOS - matrix: macos - runs-on: - arm: [macOS, ARM64] - intel: [macos-latest] - python: - major-dot-minor: '3.8' - cibw-build: 'cp38-*' - matrix: '3.8' - arch: - name: ARM - matrix: arm - - steps: - - name: Clean workspace - uses: Chia-Network/actions/clean-workspace@main - - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: chia-network/actions/setup-python@main - with: - python-version: ${{ matrix.python.major-dot-minor }} - - - name: Install pipx - run: | - pip install pipx - - - name: Build and test - env: - CIBW_PRERELEASE_PYTHONS: True - CIBW_BUILD_VERBOSITY_MACOS: 0 - CIBW_BUILD_VERBOSITY_LINUX: 0 - CIBW_BUILD_VERBOSITY_WINDOWS: 0 - CIBW_BUILD: ${{ matrix.python.cibw-build }} - CIBW_SKIP: '*-manylinux_i686 *-win32 *-musllinux_*' - CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.python.manylinux['arm'] }} - CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.python.manylinux['intel'] }} - CIBW_ENVIRONMENT_LINUX: "PATH=/project/cmake-3.14.3-Linux-`uname -m`/bin:$PATH" - CIBW_BEFORE_ALL_LINUX: > - yum -y install epel-release - && echo "epel-release installed" - && yum -y install lzip - && echo "lzip installed" - && curl -L https://github.com/Kitware/CMake/releases/download/v3.14.3/cmake-3.14.3-Linux-`uname -m`.sh > cmake.sh - && yes | sh cmake.sh | cat - && rm -f /usr/bin/cmake - && curl -L https://gmplib.org/download/gmp/gmp-6.2.1.tar.lz | tar x --lzip - && cp contrib/gmp-patch-6.2.1/longlong.h gmp-6.2.1/ - && cp contrib/gmp-patch-6.2.1/compat.c gmp-6.2.1/ - && cd gmp-6.2.1 && ./configure --enable-fat - && make && make install && cd .. && rm -rf gmp-6.2.1 - && cmake --version - && uname -a - CIBW_BEFORE_BUILD_LINUX: > - python -m pip install --upgrade pip - CIBW_ARCHS_MACOS: ${{ matrix.os.cibw-archs-macos[matrix.arch.matrix] }} - CIBW_BEFORE_ALL_MACOS: > - brew install gmp boost cmake - CIBW_BEFORE_BUILD_MACOS: > - python -m pip install --upgrade pip - CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=10.14" - CIBW_REPAIR_WHEEL_COMMAND_MACOS: > - pip uninstall -y delocate && pip install git+https://github.com/Chia-Network/delocate.git - && delocate-listdeps {wheel} && delocate-wheel -v {wheel} - && cp {wheel} {dest_dir} - CIBW_BEFORE_ALL_WINDOWS: > - curl -L https://download.libsodium.org/libsodium/releases/libsodium-1.0.18-stable-msvc.zip > libsodium-1.0.18-stable-msvc.zip - && 7z x libsodium-1.0.18-stable-msvc.zip - && git clone https://github.com/Chia-Network/relic_ietf_64.git - && ls -l relic_ietf_64 - && git clone https://github.com/Chia-Network/mpir_gc_x64.git - && ls -l mpir_gc_x64 - CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: > - ls -l mpir_gc_x64 && pip uninstall -y delocate - && pip install git+https://github.com/Chia-Network/delocate.git - && delocate-wheel -v -i mpir_gc_x64/mpir.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_gc.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_broadwell.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_broadwell_avx.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_bulldozer.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_haswell.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_piledriver.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_sandybridge.dll {wheel} - && delocate-wheel -v -i mpir_gc_x64/mpir_skylake_avx.dll {wheel} - && cp {wheel} {dest_dir} - CIBW_TEST_REQUIRES: pytest - CIBW_TEST_COMMAND: py.test -v {project}/python-bindings/test.py - run: - pipx run --spec='cibuildwheel==2.9.0' cibuildwheel --output-dir dist 2>&1 - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: packages - path: ./dist - - build-sdist: - name: sdist - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} - runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} - strategy: - fail-fast: false - matrix: - os: - - name: Ubuntu - matrix: ubuntu - runs-on: - arm: [Linux, ARM64] - intel: [ubuntu-latest] - python: - - major-dot-minor: '3.9' - matrix: '3.9' - arch: - - name: Intel - matrix: intel - - steps: - - name: Clean workspace - uses: Chia-Network/actions/clean-workspace@main - - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: Chia-Network/actions/setup-python@main - with: - python-version: ${{ matrix.python.major-dot-minor }} - - - name: Build source distribution - run: | - pip install build - python -m build --sdist --outdir dist . - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - name: packages - path: ./dist - - check: - name: Check - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} - runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} - strategy: - fail-fast: false - matrix: - os: - - name: Ubuntu - matrix: ubuntu - runs-on: - arm: [Linux, ARM64] - intel: [ubuntu-latest] - python: - - major-dot-minor: '3.9' - matrix: '3.9' - arch: - - name: Intel - matrix: intel - - steps: - - name: Clean workspace - uses: Chia-Network/actions/clean-workspace@main - - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: Chia-Network/actions/setup-python@main - with: - python-version: ${{ matrix.python.major-dot-minor }} - - - name: flake8 - run: | - pip install flake8 - flake8 src setup.py python-bindings python-impl - - name: mypy - run: | - pip install mypy - mypy --config-file mypi.ini python-bindings python-impl - upload: - name: Upload to PyPI - ${{ matrix.os.name }} ${{ matrix.python.major-dot-minor }} ${{ matrix.arch.name }} - runs-on: ${{ matrix.os.runs-on[matrix.arch.matrix] }} - needs: - - build-wheels - - build-sdist - - check - strategy: - fail-fast: false - matrix: - os: - - name: Ubuntu - matrix: ubuntu - runs-on: - arm: [Linux, ARM64] - intel: [ubuntu-latest] - python: - - major-dot-minor: '3.9' - matrix: '3.9' - arch: - - name: Intel - matrix: intel - - steps: - - name: Clean workspace - uses: Chia-Network/actions/clean-workspace@main - - - name: Checkout code - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: Chia-Network/actions/setup-python@main - with: - python-version: ${{ matrix.python.major-dot-minor }} - - - name: Download artifacts - uses: actions/download-artifact@v3 - with: - name: packages - path: ./dist - - - name: Test for secrets access - id: check_secrets - shell: bash - run: | - unset HAS_SECRET - if [ -n "$SECRET" ]; then HAS_SECRET='true' ; fi - echo ::set-output name=HAS_SECRET::${HAS_SECRET} - env: - SECRET: "${{ secrets.test_pypi_password }}" - - - name: Install twine - run: pip install twine - - - name: Publish distribution to PyPI - if: startsWith(github.event.ref, 'refs/tags') && steps.check_secrets.outputs.HAS_SECRET - env: - TWINE_USERNAME: __token__ - TWINE_NON_INTERACTIVE: 1 - TWINE_PASSWORD: ${{ secrets.pypi_password }} - run: twine upload --non-interactive --skip-existing --verbose 'dist/*' - - - name: Publish distribution to Test PyPI - if: steps.check_secrets.outputs.HAS_SECRET - env: - TWINE_REPOSITORY_URL: https://test.pypi.org/legacy/ - TWINE_USERNAME: __token__ - TWINE_NON_INTERACTIVE: 1 - TWINE_PASSWORD: ${{ secrets.test_pypi_password }} - run: twine upload --non-interactive --skip-existing --verbose 'dist/*' diff --git a/.github/workflows/js-bindings.yml b/.github/workflows/js-bindings.yml deleted file mode 100644 index 887c8df3d..000000000 --- a/.github/workflows/js-bindings.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Build & Publish JS Bindings - -on: - push: - branches: - - main - tags: - - '**' - pull_request: - branches: - - '**' - -concurrency: - # SHA is added to the end if on `main` to let all main workflows run - group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main') && github.sha || '' }} - cancel-in-progress: true - -jobs: - js_bindings: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install emsdk - uses: mymindstorm/setup-emsdk@v11 - - - name: Get the version - id: version_info - run: echo ::set-output name=SOURCE_TAG::${GITHUB_REF#refs/tags/} - - - name: Update version in package.json - if: startsWith(github.ref, 'refs/tags/') - working-directory: ${{ github.workspace }}/js-bindings - env: - SOURCE_TAG: ${{ steps.version_info.outputs.SOURCE_TAG }} - run: | - jq --arg VER "$SOURCE_TAG" '.version=$VER' package.json > temp.json && mv temp.json package.json - - - name: Build JS - run: ./js_build.sh - - - name: Publish - if: startsWith(github.ref, 'refs/tags/') - working-directory: ${{ github.workspace }}/js_build/js-bindings - env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: | - echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc - npm publish --access public - - - name: Cleanup - if: always() - run: - rm ${{ github.workspace }}/js_build/js-bindings/.npmrc || true diff --git a/.github/workflows/relic-nightly.yml b/.github/workflows/relic-nightly.yml deleted file mode 100644 index 2a9ef6715..000000000 --- a/.github/workflows/relic-nightly.yml +++ /dev/null @@ -1,78 +0,0 @@ -name: Build and Test with Relic Nightly - -on: - schedule: - - cron: "0 11 * * *" - workflow_dispatch: - -concurrency: - # SHA is added to the end if on `main` to let all main workflows run - group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main') && github.sha || '' }} - cancel-in-progress: true - -jobs: - build_wheels: - name: Build and Test with Relic Nightly - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-latest, ubuntu-latest] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Ubuntu build C++ and test Relic at origin/main - if: startsWith(matrix.os, 'ubuntu') - run: | - echo "Relic origin/main commit:" - curl -H "application/vnd.github.v3.sha" \ - https://api.github.com/repos/relic-toolkit/relic/commits/main | \ - head -10 - sudo apt-get update - sudo apt-get install snap -y - sudo apt-get remove --purge cmake -y - sudo snap install cmake --classic - hash -r - cmake --version - export RELIC_MAIN=1 - mkdir -p build - cd build - cmake ../ - cmake --build . -- -j 6 - echo "Running ./src/runtest" - ./src/runtest - - - name: Mac OS build C++ and test - if: startsWith(matrix.os, 'macos') - run: | - ls -l - export MACOSX_DEPLOYMENT_TARGET=10.14 - export RELIC_MAIN=1 - mkdir -p build - ls -l build - cd build - cmake ../ - cmake --build . -- -j 6 - echo "Running ./src/runtest" - ./src/runtest - - - uses: actions/setup-python@v2 - name: Install Python - with: - python-version: '3.8' - - - name: Test pure python implementation - run: | - python python-impl/impl-test.py - - - name: Install emsdk - uses: mymindstorm/setup-emsdk@v11 - - - name: Test javascript bindings - run: | - emcc -v - export RELIC_MAIN=1 - sh emsdk_build.sh - sh js_test.sh From e86634108edd5b06c7f98f05348390ce4288bf61 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:20:55 +0530 Subject: [PATCH 05/31] refactor: move from `python-bindings` to `binds/python` --- CMakeLists.txt | 2 +- MANIFEST.in | 2 +- README.md | 2 +- {python-bindings => binds/python}/CMakeLists.txt | 2 +- {python-bindings => binds/python}/README.md | 0 {python-bindings => binds/python}/benchmark.py | 0 {python-bindings => binds/python}/pythonbindings.cpp | 0 {python-bindings => binds/python}/test.py | 0 setup.py | 2 +- 9 files changed, 5 insertions(+), 5 deletions(-) rename {python-bindings => binds/python}/CMakeLists.txt (88%) rename {python-bindings => binds/python}/README.md (100%) rename {python-bindings => binds/python}/benchmark.py (100%) rename {python-bindings => binds/python}/pythonbindings.cpp (100%) rename {python-bindings => binds/python}/test.py (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f413f97cc..09e8685d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,6 +145,6 @@ else() # emscripten can't build python bindings, it produces only javascript # add_subdirectory(contrib/pybind11) if(BUILD_BLS_PYTHON_BINDINGS) - add_subdirectory(python-bindings) + add_subdirectory(binds/python) endif() endif() diff --git a/MANIFEST.in b/MANIFEST.in index 3063e7883..554ebf13d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,5 @@ include README.md LICENSE global-include CMakeLists.txt *.cmake recursive-include cmake_modules * recursive-include src * -recursive-include python-bindings * +recursive-include binds/python * recursive-include contrib * \ No newline at end of file diff --git a/README.md b/README.md index 89d07add4..b126b1f76 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ `bls-signatures` is a cross-platform library implementing BLS12-381 primitives for Dash built on the [`relic`](https://github.com/relic-toolkit/relic) toolkit with bindings available in -[Python](./python-bindings), [Rust](./rust-bindings/), [Go](./go-bindings/) and [Javascript](./js-bindings). +[Python](./binds/python), [Rust](./rust-bindings/), [Go](./go-bindings/) and [Javascript](./js-bindings). ## Dependencies diff --git a/python-bindings/CMakeLists.txt b/binds/python/CMakeLists.txt similarity index 88% rename from python-bindings/CMakeLists.txt rename to binds/python/CMakeLists.txt index 2726399ed..e6115ee09 100644 --- a/python-bindings/CMakeLists.txt +++ b/binds/python/CMakeLists.txt @@ -10,7 +10,7 @@ FetchContent_MakeAvailable(pybind11) include_directories( ${INCLUDE_DIRECTORIES} ${CMAKE_CURRENT_SOURCE_DIR} - ${CMAKE_CURRENT_SOURCE_DIR}/../include + ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) pybind11_add_module(blspy ${CMAKE_CURRENT_SOURCE_DIR}/pythonbindings.cpp) diff --git a/python-bindings/README.md b/binds/python/README.md similarity index 100% rename from python-bindings/README.md rename to binds/python/README.md diff --git a/python-bindings/benchmark.py b/binds/python/benchmark.py similarity index 100% rename from python-bindings/benchmark.py rename to binds/python/benchmark.py diff --git a/python-bindings/pythonbindings.cpp b/binds/python/pythonbindings.cpp similarity index 100% rename from python-bindings/pythonbindings.cpp rename to binds/python/pythonbindings.cpp diff --git a/python-bindings/test.py b/binds/python/test.py similarity index 100% rename from python-bindings/test.py rename to binds/python/test.py diff --git a/setup.py b/setup.py index 7b4c0bfbb..6410ceacc 100644 --- a/setup.py +++ b/setup.py @@ -96,7 +96,7 @@ def __str__(self): "src/schemes.cpp", "src/privatekey.cpp", "src/bls.cpp", - "python-bindings/pythonbindings.cpp", + "binds/python/pythonbindings.cpp", ], include_dirs=[ # Path to pybind11 headers From d65ba6b062f88160f0ad69b59c62a3801ad51cb8 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:14:42 +0530 Subject: [PATCH 06/31] refactor: avoid clash with upstream, rename to `dashbls`, update author --- .gitignore | 4 ++-- binds/python/CMakeLists.txt | 5 +++-- binds/python/README.md | 4 ++-- binds/python/benchmark.py | 2 +- binds/python/pythonbindings.cpp | 2 +- binds/python/test.py | 2 +- setup.py | 24 ++++++++++++------------ 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index bec3896a5..a758c18de 100644 --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,10 @@ src/MakefilE src/cmake_install.cmake src/CMakeFile build/* -blspy.egg-info +dashbls.egg-info dist python-impl/__pycache__/ -blspy.*.so +dashbls.*.so .mypy_cache/ .pytest_chache/ .eggs/ diff --git a/binds/python/CMakeLists.txt b/binds/python/CMakeLists.txt index e6115ee09..926144ceb 100644 --- a/binds/python/CMakeLists.txt +++ b/binds/python/CMakeLists.txt @@ -13,5 +13,6 @@ include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) -pybind11_add_module(blspy ${CMAKE_CURRENT_SOURCE_DIR}/pythonbindings.cpp) -target_link_libraries(blspy PRIVATE dashbls) +pybind11_add_module(dashbls_py ${CMAKE_CURRENT_SOURCE_DIR}/pythonbindings.cpp) +set_target_properties(dashbls_py PROPERTIES OUTPUT_NAME dashbls) +target_link_libraries(dashbls_py PRIVATE dashbls) diff --git a/binds/python/README.md b/binds/python/README.md index 0de31c9a8..7af234005 100644 --- a/binds/python/README.md +++ b/binds/python/README.md @@ -5,7 +5,7 @@ Use the full power and efficiency of the C++ bls library, but in a few lines of ## Install ```bash -pip3 install blspy +pip3 install dashbls ``` @@ -24,7 +24,7 @@ Then, to use: ## Import the library ```python -from blspy import (PrivateKey, Util, AugSchemeMPL, PopSchemeMPL, +from dashbls import (PrivateKey, Util, AugSchemeMPL, PopSchemeMPL, G1Element, G2Element) ``` diff --git a/binds/python/benchmark.py b/binds/python/benchmark.py index 51feebd05..8ba3b7c0a 100644 --- a/binds/python/benchmark.py +++ b/binds/python/benchmark.py @@ -2,7 +2,7 @@ import time import secrets -from blspy import ( +from dashbls import ( AugSchemeMPL, G1Element, G2Element, diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index 4ba2acb83..e3df04d58 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -36,7 +36,7 @@ inline int PyLong_AsByteArray(PyLongObject* obj, uint8_t* buf, Py_ssize_t size, } } // anonymous namespace -PYBIND11_MODULE(blspy, m) +PYBIND11_MODULE(dashbls, m) { py::class_(m, "PrivateKey") .def_property_readonly_static( diff --git a/binds/python/test.py b/binds/python/test.py index 971d87859..0c2fdf172 100644 --- a/binds/python/test.py +++ b/binds/python/test.py @@ -3,7 +3,7 @@ import time from copy import deepcopy -from blspy import ( +from dashbls import ( AugSchemeMPL, BasicSchemeMPL, G1Element, diff --git a/setup.py b/setup.py index 6410ceacc..3751f76bf 100644 --- a/setup.py +++ b/setup.py @@ -90,7 +90,7 @@ def __str__(self): ext_modules = [ Extension( - "blspy", + "dashbls", [ "src/elements.cpp", "src/schemes.cpp", @@ -188,13 +188,13 @@ def build_extensions(self): if platform.system() == "Windows": setup( - name="blspy", - author="Mariano Sorgente", - author_email="mariano@chia.net", - description="BLS signatures in c++ (with python bindings)", + name="dashbls", + author="The Dash Core developers", + author_email="contact@dash.org", + description="Python 3.x binds for Dash's bls-signatures", long_description=open("README.md").read(), long_description_content_type="text/markdown", - url="https://github.com/Chia-Network/bls-signatures", + url="https://github.com/dashpay/bls-signatures", python_requires=">=3.7", setup_requires=["pybind11>=2.10.0"], install_requires=["pybind11>=2.10.0"], @@ -204,16 +204,16 @@ def build_extensions(self): ) else: setup( - name="blspy", - author="Mariano Sorgente", - author_email="mariano@chia.net", - description="BLS signatures in c++ (python bindings)", + name="dashbls", + author="The Dash Core developers", + author_email="contact@dash.org", + description="Python 3.x binds for Dash's bls-signatures", python_requires=">=3.7", install_requires=["wheel"], long_description=open("README.md").read(), long_description_content_type="text/markdown", - url="https://github.com/Chia-Network/bls-signatures", - ext_modules=[CMakeExtension("blspy", ".")], + url="https://github.com/dashpay/bls-signatures", + ext_modules=[CMakeExtension("dashbls", ".")], cmdclass=dict(build_ext=CMakeBuild), zip_safe=False, ) From 86f4065c3792e5e92b03dd1848297a726bea0b49 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 02:22:43 +0530 Subject: [PATCH 07/31] chore: partially import `Python.gitignore` --- .gitignore | 78 ++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index a758c18de..28ba8189d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,13 +8,6 @@ src/MakefilE src/cmake_install.cmake src/CMakeFile build/* -dashbls.egg-info -dist -python-impl/__pycache__/ -dashbls.*.so -.mypy_cache/ -.pytest_chache/ -.eggs/ cmake-build-debug/ js_build @@ -48,7 +41,6 @@ runbench.* *.bak *.rej *.orig -*.pyc *.o *.o-* *.patch @@ -115,3 +107,73 @@ autom4te.cache /stamp-h1 /ltmain.sh /texinfo.tex + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +*.lcov +.hypothesis/ +.pytest_cache/ +cover/ + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Cython debug symbols +cython_debug/ + +# Temporary file for partial code execution +tempCodeRunnerFile.py + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc From 02fed5f16f7b7c130d7fc40b982c82a7c367e332 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:50:49 +0530 Subject: [PATCH 08/31] build: declare packaging metadata in `pyproject.toml`, add `NOTICE` --- NOTICE | 153 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 15 +++++ pyproject.toml | 18 +++++- setup.py | 18 ------ 4 files changed, 185 insertions(+), 19 deletions(-) create mode 100644 NOTICE diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..a8f984371 --- /dev/null +++ b/NOTICE @@ -0,0 +1,153 @@ +dashbls (https://github.com/dashpay/bls-signatures) + +Copyright (c) 2018-present, Chia Network, Inc. +Copyright (c) 2021-present, The Dash Core developers. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +-------------------------------------------------------------------------- + +relic (https://github.com/relic-toolkit/relic) + +Copyright (c) 2009 RELIC Authors + +RELIC is free software; you can redistribute it and/or modify it under the +terms of the version 2.1 (or later) of the GNU Lesser General Public License +as published by the Free Software Foundation; or version 2.0 of the Apache +License as published by the Apache Software Foundation. See the LICENSE files +for more details. + +RELIC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR +A PARTICULAR PURPOSE. See the LICENSE files for more details. + +You should have received a copy of the GNU Lesser General Public or the +Apache License along with RELIC. If not, see +or . + +-------------------------------------------------------------------------- + +mimalloc (https://github.com/microsoft/mimalloc) + +MIT License + +Copyright (c) 2018-2025 Microsoft Corporation, Daan Leijen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +-------------------------------------------------------------------------- + +GNU MP (https://gmplib.org/) + +Copyright 1991-present Free Software Foundation, Inc. + +The GNU MP Library is free software; you can redistribute it and/or modify +it under the terms of either: + + * the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3 of the License, or (at your option) any + later version. + +or + + * the GNU General Public License as published by the Free Software + Foundation; either version 2 of the License, or (at your option) any + later version. + +or both in parallel, as here. + +The GNU MP Library is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License and +the GNU Lesser General Public License for more details. + +You should have received copies of the GNU General Public License and the +GNU Lesser General Public License along with the GNU MP Library. If not, see +https://www.gnu.org/licenses/. + +-------------------------------------------------------------------------- + +pybind11 (https://github.com/pybind/pybind11) + +Copyright (c) 2016 Wenzel Jakob , All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +-------------------------------------------------------------------------- + +Catch2 (https://github.com/catchorg/Catch2) + +Copyright (c) 2022 Two Blue Cubes Ltd. All rights reserved. + +Boost Software License - Version 1.0 - August 17th, 2003 + +Permission is hereby granted, free of charge, to any person or organization +obtaining a copy of the software and accompanying documentation covered by +this license (the "Software") to use, reproduce, display, distribute, +execute, and transmit the Software, and to prepare derivative works of the +Software, and to permit third-parties to whom the Software is furnished to +do so, all subject to the following: + +The copyright notices in the Software and this entire statement, including +the above license grant, this restriction and the following disclaimer, +must be included in all copies of the Software, in whole or in part, and +all derivative works of the Software, unless such copies or derivative +works are solely in the form of machine-executable object code generated by +a source language processor. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT +SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE +FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index b126b1f76..989582cd1 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,19 @@ cmake --build . --parallel 4 ```text Copyright (c) 2018-present, Chia Network, Inc. Copyright (c) 2021-present, The Dash Core developers. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. ``` + +The above license terms are available in [`LICENSE`](./LICENSE) and licensing terms for dependencies are +available in [`NOTICE`](./NOTICE) diff --git a/pyproject.toml b/pyproject.toml index c320f0e05..5f33aef7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,21 @@ +[project] +name = "dashbls" +description = "Python 3.x binds for Dash's bls-signatures" +authors = [{ name = "Dash Core Developers" }] +keywords = ["bls", "signatures", "bls12-381", "dash", "cryptography"] +license = "Apache-2.0" +license-files = ["LICENSE", "NOTICE"] +readme = "README.md" +requires-python = ">=3.9" +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/dashpay/bls-signatures" +Source = "https://github.com/dashpay/bls-signatures" +Issues = "https://github.com/dashpay/bls-signatures/issues" + [build-system] -requires = ["setuptools>=42", "wheel", "setuptools_scm[toml]>=3.5.0", "pybind11"] +requires = ["setuptools>=77", "setuptools_scm>=8", "pybind11>=2.13"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] diff --git a/setup.py b/setup.py index 3751f76bf..60332822a 100644 --- a/setup.py +++ b/setup.py @@ -188,31 +188,13 @@ def build_extensions(self): if platform.system() == "Windows": setup( - name="dashbls", - author="The Dash Core developers", - author_email="contact@dash.org", - description="Python 3.x binds for Dash's bls-signatures", - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - url="https://github.com/dashpay/bls-signatures", - python_requires=">=3.7", setup_requires=["pybind11>=2.10.0"], - install_requires=["pybind11>=2.10.0"], ext_modules=ext_modules, cmdclass={"build_ext": BuildExt}, zip_safe=False, ) else: setup( - name="dashbls", - author="The Dash Core developers", - author_email="contact@dash.org", - description="Python 3.x binds for Dash's bls-signatures", - python_requires=">=3.7", - install_requires=["wheel"], - long_description=open("README.md").read(), - long_description_content_type="text/markdown", - url="https://github.com/dashpay/bls-signatures", ext_modules=[CMakeExtension("dashbls", ".")], cmdclass=dict(build_ext=CMakeBuild), zip_safe=False, From 82f904e413f2b2d349bc41015783d67246b414aa Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:23:29 +0530 Subject: [PATCH 09/31] fix: uniformly enforce CMake 3.18 requirement --- CMakeLists.txt | 2 +- depends/catch2/CMakeLists.txt | 2 +- js-bindings/CMakeLists.txt | 2 +- setup.py | 10 ++++------ 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 09e8685d2..a224da20b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 3.14.0 FATAL_ERROR) +CMAKE_MINIMUM_REQUIRED(VERSION 3.18.0 FATAL_ERROR) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_C_STANDARD 99) diff --git a/depends/catch2/CMakeLists.txt b/depends/catch2/CMakeLists.txt index 8c26a03af..efbbe0f7d 100644 --- a/depends/catch2/CMakeLists.txt +++ b/depends/catch2/CMakeLists.txt @@ -1,4 +1,4 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 3.14.0 FATAL_ERROR) +CMAKE_MINIMUM_REQUIRED(VERSION 3.18.0 FATAL_ERROR) set(PROJECT_NAME "catch2") diff --git a/js-bindings/CMakeLists.txt b/js-bindings/CMakeLists.txt index f053eb2ea..5790f3a11 100644 --- a/js-bindings/CMakeLists.txt +++ b/js-bindings/CMakeLists.txt @@ -1,4 +1,4 @@ -CMAKE_MINIMUM_REQUIRED(VERSION 3.14.0 FATAL_ERROR) +CMAKE_MINIMUM_REQUIRED(VERSION 3.18.0 FATAL_ERROR) set(CMAKE_CXX_STANDARD 17) include_directories( diff --git a/setup.py b/setup.py index 60332822a..248660783 100644 --- a/setup.py +++ b/setup.py @@ -27,12 +27,10 @@ def run(self): + ", ".join(e.name for e in self.extensions) ) - if platform.system() == "Windows": - cmake_version = LooseVersion( - re.search(r"version\s*([\d.]+)", out.decode()).group(1) - ) - if cmake_version < "3.1.0": - raise RuntimeError("CMake >= 3.1.0 is required on Windows") + version_str = re.search(r"version\s*([\d.]+)", out.decode()).group(1) + cmake_version = tuple(int(part) for part in version_str.split(".")) + if cmake_version < (3, 18, 0): + raise RuntimeError("CMake >= 3.18.0 is required") for ext in self.extensions: self.build_extension(ext) From ad09335dd2ac5f502af029aa233fad45f91f7f24 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:34:58 +0530 Subject: [PATCH 10/31] build: require Python 3.10 or higher --- .python-version | 1 + pyproject.toml | 2 +- setup.py | 14 ++++---------- 3 files changed, 6 insertions(+), 11 deletions(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 000000000..eae0123de --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10.19 diff --git a/pyproject.toml b/pyproject.toml index 5f33aef7d..2b4955797 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ keywords = ["bls", "signatures", "bls12-381", "dash", "cryptography"] license = "Apache-2.0" license-files = ["LICENSE", "NOTICE"] readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.10" dynamic = ["version"] [project.urls] diff --git a/setup.py b/setup.py index 248660783..d757df00b 100644 --- a/setup.py +++ b/setup.py @@ -4,10 +4,10 @@ import re import subprocess import sys -from distutils.version import LooseVersion -from setuptools import Extension, setup, setuptools +from setuptools import Extension, setup from setuptools.command.build_ext import build_ext +from setuptools.errors import CompileError class CMakeExtension(Extension): @@ -115,8 +115,6 @@ def __str__(self): ] -# As of Python 3.6, CCompiler has a `has_flag` method. -# cf http://bugs.python.org/issue26689 def has_flag(compiler, flagname): """Return a boolean indicating whether a flag name is supported on the specified compiler. @@ -127,7 +125,7 @@ def has_flag(compiler, flagname): f.write("int main (int argc, char **argv) { return 0; }") try: compiler.compile([f.name], extra_postargs=[flagname]) - except setuptools.distutils.errors.CompileError: + except CompileError: return False return True @@ -173,11 +171,7 @@ def build_extensions(self): if has_flag(self.compiler, "-fvisibility=hidden"): opts.append("-fvisibility=hidden") elif ct == "msvc": - if sys.version_info < (3, 9): - ver_flag = '/DVERSION_INFO=\"%s\"' - else: - ver_flag = '-DVERSION_INFO="%s"' - opts.append(ver_flag % self.distribution.get_version()) + opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version()) for ext in self.extensions: ext.extra_compile_args = opts ext.extra_link_args = link_opts From 211d2e89a9c5117fec4308335044c3cafae51efe Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:17:10 +0530 Subject: [PATCH 11/31] build: unify all platforms behind CMake builds --- setup.py | 170 ++++++++++++------------------------------------------- 1 file changed, 36 insertions(+), 134 deletions(-) diff --git a/setup.py b/setup.py index d757df00b..67cb913b7 100644 --- a/setup.py +++ b/setup.py @@ -1,37 +1,53 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Build configuration for the dashbls package.""" + import os import platform import re import subprocess import sys +import sysconfig from setuptools import Extension, setup from setuptools.command.build_ext import build_ext -from setuptools.errors import CompileError class CMakeExtension(Extension): def __init__(self, name, sourcedir=""): - Extension.__init__(self, name, sources=["./"]) + super().__init__(name, sources=[]) self.sourcedir = os.path.abspath(sourcedir) class CMakeBuild(build_ext): + CMAKE_MINIMUM = (3, 18, 0) + def run(self): try: - out = subprocess.check_output(["cmake", "--version"]) + out = subprocess.check_output(["cmake", "--version"]).decode() except OSError: + raise RuntimeError("CMake must be installed to build dashbls") + found = re.search(r"version\s*([\d.]+)", out) + if found is None: + raise RuntimeError("cannot read a version out of: " + out.strip()) + parts = [int(part) for part in found.group(1).split(".")[:3] if part] + while len(parts) < len(self.CMAKE_MINIMUM): + parts.append(0) + version = tuple(parts) + if version < self.CMAKE_MINIMUM: raise RuntimeError( - "CMake must be installed to build" - + " the following extensions: " - + ", ".join(e.name for e in self.extensions) + "CMake >= {} is required, found {}".format( + ".".join(str(p) for p in self.CMAKE_MINIMUM), found.group(1) + ) ) - - version_str = re.search(r"version\s*([\d.]+)", out.decode()).group(1) - cmake_version = tuple(int(part) for part in version_str.split(".")) - if cmake_version < (3, 18, 0): - raise RuntimeError("CMake >= 3.18.0 is required") - for ext in self.extensions: self.build_extension(ext) @@ -39,7 +55,9 @@ def build_extension(self, ext): extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) cmake_args = [ "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + extdir, + "-DMULTI=", "-DPYTHON_EXECUTABLE=" + sys.executable, + "-DPYTHON_EXTENSION_SUFFIX=" + (sysconfig.get_config_var("EXT_SUFFIX") or ""), ] cfg = "Debug" if self.debug else "Release" @@ -70,124 +88,8 @@ def build_extension(self, ext): ) -class get_pybind_include(object): - """Helper class to determine the pybind11 include path - - The purpose of this class is to postpone importing pybind11 - until it is actually installed, so that the ``get_include()`` - method can be invoked.""" - - def __init__(self, user=False): - self.user = user - - def __str__(self): - import pybind11 - - return pybind11.get_include(self.user) - - -ext_modules = [ - Extension( - "dashbls", - [ - "src/elements.cpp", - "src/schemes.cpp", - "src/privatekey.cpp", - "src/bls.cpp", - "binds/python/pythonbindings.cpp", - ], - include_dirs=[ - # Path to pybind11 headers - get_pybind_include(), - get_pybind_include(user=True), - "relic_ietf_64/include", - "mpir_gc_x64", - "libsodium/include", - ], - library_dirs=[ - "relic_ietf_64", - "mpir_gc_x64", - "libsodium/x64/Release/v142/static", - ], - libraries=["relic_s", "Advapi32", "mpir", "libsodium"], - language="c++", - ), -] - - -def has_flag(compiler, flagname): - """Return a boolean indicating whether a flag name is supported on - the specified compiler. - """ - import tempfile - - with tempfile.NamedTemporaryFile("w", suffix=".cpp") as f: - f.write("int main (int argc, char **argv) { return 0; }") - try: - compiler.compile([f.name], extra_postargs=[flagname]) - except CompileError: - return False - return True - - -def cpp_flag(compiler): - """Return the -std=c++[11/14/17] compiler flag. - - The newer version is prefered over c++11 (when it is available). - """ - flags = ["-std=c++17", "-std=c++14", "-std=c++11"] - - for flag in flags: - if has_flag(compiler, flag): - return flag - - raise RuntimeError("Unsupported compiler -- at least C++11 support " "is needed!") - - -class BuildExt(build_ext): - """A custom build extension for adding compiler-specific options.""" - - c_opts = { - "msvc": ["/EHsc", "/std:c++17", "/DBLSALLOC_SODIUM=1", "/DSODIUM_STATIC"], - "unix": [], - } - l_opts = { - "msvc": [], - "unix": [], - } - - if sys.platform == "darwin": - darwin_opts = ["-stdlib=libc++", "-mmacosx-version-min=10.14"] - c_opts["unix"] += darwin_opts - l_opts["unix"] += darwin_opts - - def build_extensions(self): - ct = self.compiler.compiler_type - opts = self.c_opts.get(ct, []) - link_opts = self.l_opts.get(ct, []) - if ct == "unix": - opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version()) - opts.append(cpp_flag(self.compiler)) - if has_flag(self.compiler, "-fvisibility=hidden"): - opts.append("-fvisibility=hidden") - elif ct == "msvc": - opts.append('-DVERSION_INFO="%s"' % self.distribution.get_version()) - for ext in self.extensions: - ext.extra_compile_args = opts - ext.extra_link_args = link_opts - build_ext.build_extensions(self) - - -if platform.system() == "Windows": - setup( - setup_requires=["pybind11>=2.10.0"], - ext_modules=ext_modules, - cmdclass={"build_ext": BuildExt}, - zip_safe=False, - ) -else: - setup( - ext_modules=[CMakeExtension("dashbls", ".")], - cmdclass=dict(build_ext=CMakeBuild), - zip_safe=False, - ) +setup( + ext_modules=[CMakeExtension("dashbls", ".")], + cmdclass=dict(build_ext=CMakeBuild), + zip_safe=False, +) From 74bb3151bd034be747f098f9192c2831886a1165 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 21:39:21 +0530 Subject: [PATCH 12/31] build: don't make target assumptions based on host, fix Windows flags --- setup.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 67cb913b7..bb179559a 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,6 @@ """Build configuration for the dashbls package.""" import os -import platform import re import subprocess import sys @@ -29,6 +28,7 @@ def __init__(self, name, sourcedir=""): class CMakeBuild(build_ext): CMAKE_MINIMUM = (3, 18, 0) + WINDOWS_GENERATOR_ARCH = {"win-amd64": "x64", "win-arm64": "ARM64", "win32": "Win32"} def run(self): try: @@ -63,13 +63,20 @@ def build_extension(self, ext): cfg = "Debug" if self.debug else "Release" build_args = ["--config", cfg] - if platform.system() == "Windows": - cmake_args += [ + if sys.platform == "win32": + target = sysconfig.get_platform() + arch = self.WINDOWS_GENERATOR_ARCH.get(target) + if arch is None: + raise RuntimeError("unsupported windows platform: " + target) + cmake_args.append( "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir) - ] - if sys.maxsize > 2 ** 32: - cmake_args += ["-A", "x64"] - build_args += ["--", "/m"] + ) + generator = os.environ.get("CMAKE_GENERATOR", "Visual Studio") + if generator.startswith("Visual Studio"): + cmake_args += ["-A", arch] + build_args += ["--", "/m"] + else: + cmake_args += ["-DCMAKE_BUILD_TYPE=" + cfg] else: cmake_args += ["-DCMAKE_BUILD_TYPE=" + cfg] build_args += ["--", "-j", "6"] From 691c6a964afba3cb1b19f9fea19cd123363dcd90 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:39:17 +0530 Subject: [PATCH 13/31] build: find `pybind11` >=2.13.6 in locally before using `FetchContent` --- binds/python/CMakeLists.txt | 26 ++++++++++++++------------ pyproject.toml | 2 +- setup.py | 6 ++++++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/binds/python/CMakeLists.txt b/binds/python/CMakeLists.txt index 926144ceb..d05b7b649 100644 --- a/binds/python/CMakeLists.txt +++ b/binds/python/CMakeLists.txt @@ -1,18 +1,20 @@ -include(FetchContent) +find_package(pybind11 2.13.6 CONFIG QUIET) -FetchContent_Declare( - pybind11 - GIT_REPOSITORY https://github.com/pybind/pybind11.git - GIT_TAG v2.13.6 -) -FetchContent_MakeAvailable(pybind11) +if(NOT pybind11_FOUND) + message(STATUS "pybind11 not found locally; fetching it") + include(FetchContent) + FetchContent_Declare( + pybind11 + GIT_REPOSITORY https://github.com/pybind/pybind11.git + GIT_TAG v2.13.6 + ) + FetchContent_MakeAvailable(pybind11) +endif() -include_directories( - ${INCLUDE_DIRECTORIES} +pybind11_add_module(dashbls_py ${CMAKE_CURRENT_SOURCE_DIR}/pythonbindings.cpp) +set_target_properties(dashbls_py PROPERTIES OUTPUT_NAME dashbls) +target_include_directories(dashbls_py PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) - -pybind11_add_module(dashbls_py ${CMAKE_CURRENT_SOURCE_DIR}/pythonbindings.cpp) -set_target_properties(dashbls_py PROPERTIES OUTPUT_NAME dashbls) target_link_libraries(dashbls_py PRIVATE dashbls) diff --git a/pyproject.toml b/pyproject.toml index 2b4955797..ef93bef01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ Source = "https://github.com/dashpay/bls-signatures" Issues = "https://github.com/dashpay/bls-signatures/issues" [build-system] -requires = ["setuptools>=77", "setuptools_scm>=8", "pybind11>=2.13"] +requires = ["setuptools>=77", "setuptools_scm>=8", "pybind11>=2.13.6"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] diff --git a/setup.py b/setup.py index bb179559a..1ae2584e1 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,12 @@ def build_extension(self, ext): "-DPYTHON_EXTENSION_SUFFIX=" + (sysconfig.get_config_var("EXT_SUFFIX") or ""), ] + try: + import pybind11 + cmake_args.append("-Dpybind11_DIR=" + pybind11.get_cmake_dir()) + except ImportError: + pass + cfg = "Debug" if self.debug else "Release" build_args = ["--config", cfg] From 5878b740e54befd5c5a80b8cbfe39f76224791c6 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:49:01 +0530 Subject: [PATCH 14/31] build: skip test, bench and honor CPU count for pybind builds --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 1ae2584e1..7212aa95c 100644 --- a/setup.py +++ b/setup.py @@ -54,6 +54,8 @@ def run(self): def build_extension(self, ext): extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) cmake_args = [ + "-DBUILD_BLS_BENCHMARKS=OFF", + "-DBUILD_BLS_TESTS=OFF", "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + extdir, "-DMULTI=", "-DPYTHON_EXECUTABLE=" + sys.executable, @@ -67,7 +69,7 @@ def build_extension(self, ext): pass cfg = "Debug" if self.debug else "Release" - build_args = ["--config", cfg] + build_args = ["--config", cfg, "--parallel", str(os.cpu_count() or 1)] if sys.platform == "win32": target = sysconfig.get_platform() @@ -85,14 +87,12 @@ def build_extension(self, ext): cmake_args += ["-DCMAKE_BUILD_TYPE=" + cfg] else: cmake_args += ["-DCMAKE_BUILD_TYPE=" + cfg] - build_args += ["--", "-j", "6"] env = os.environ.copy() env["CXXFLAGS"] = '{} -DVERSION_INFO=\\"{}\\"'.format( env.get("CXXFLAGS", ""), self.distribution.get_version() ) - if not os.path.exists(self.build_temp): - os.makedirs(self.build_temp) + os.makedirs(self.build_temp, exist_ok=True) subprocess.check_call( ["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env ) From a6fbf854b293940912bd4237697eb181cbd970bd Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:47:46 +0530 Subject: [PATCH 15/31] build: drop cruft from python bind bundle --- MANIFEST.in | 50 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 554ebf13d..62cc22eae 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,6 +1,44 @@ -include README.md LICENSE -global-include CMakeLists.txt *.cmake -recursive-include cmake_modules * -recursive-include src * -recursive-include binds/python * -recursive-include contrib * \ No newline at end of file +# Unused sources +exclude src/test.cpp src/test-bench.cpp + +# Foreign languages +prune js-bindings +prune go-bindings +prune rust-bindings +exclude emsdk_*.sh js_*.sh apple.rust.deps.sh + +# Vendored test dependency +prune depends/catch2 + +# Vendored dependency extras +prune depends/mimalloc/doc +prune depends/mimalloc/docs +prune depends/mimalloc/ide +prune depends/mimalloc/test +prune depends/mimalloc/contrib +prune depends/relic/.github +prune depends/relic/art +prune depends/relic/bench +prune depends/relic/demo +prune depends/relic/doc +prune depends/relic/preset +prune depends/relic/test +prune depends/relic/tools + +# Autotools build +prune build-aux +prune autom4te.cache +exclude Makefile.am Makefile.*.include Makefile.in Makefile configure.ac aclocal.m4 autogen.sh + +# dot{dir,file}s +prune .github +prune .vscode +prune .claude +exclude .clang-format .python-version .gitignore + +# Build artifacts +prune build +prune .libs +global-exclude *.o *.a *.lo *.la *.so *.dylib *.dll *.pyc +global-exclude *.orig *.rej *.bak +global-exclude *.egg-info/* From 7373fde99e895b6b3dfa5e1ce8e4f8c4acfa46bf Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:54:42 +0530 Subject: [PATCH 16/31] lint: switch to `ruff` for Python enforcement, clean up scripts --- .vscode/extensions.json | 7 +++++ .vscode/settings.json | 8 +++++ README.md | 37 +++++++++++++++++++++++ binds/python/benchmark.py | 32 ++++++++++++-------- binds/python/test.py | 60 ++++++++++++++++--------------------- pyproject.toml | 34 +++++++++++++++++++++ setup.py | 62 ++++++++++++++++++++++++--------------- 7 files changed, 168 insertions(+), 72 deletions(-) create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json mode change 100644 => 100755 binds/python/benchmark.py mode change 100644 => 100755 binds/python/test.py diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 000000000..622b5920a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "ms-python.python", + "ms-python.vscode-pylance", + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..a4d694f55 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "[python]": { + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": true, + "editor.rulers": [88, 100] + }, + "editor.minimap.enabled": true, +} diff --git a/README.md b/README.md index 989582cd1..0ec021a6b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,10 @@ Additionally, the following dependencies are supplied by the codebase ## Build library +> [!NOTE] +> Multi-config generators (like Visual Studio) place the binaries under a per-configuration directory, so on Windows the +> executables are at `src\Release\runtest.exe` and `src\Release\runbench.exe`. + ```sh # Create scratchpad directory mkdir build && cd build @@ -45,6 +49,39 @@ cmake --build . --parallel 4 ./src/runbench ``` +## Build Python binds + +Our Python binds target Python 3.10 or higher; they depend on + +* [`pybind11`](https://github.com/pybind/pybind11) (bridging C++ and Python) +* [`ruff`](https://github.com/astral-sh/ruff) (linting, part of optional `[.dev]` dependency group) + +> [!NOTE] +> We recommend using programs like [`uv`](https://github.com/astral-sh/uv) to manage your virtualenv (`venv`) to prevent +> cross-contamination with Python-based native packages or other Python projects. + +```sh +# Create a new venv named dashbls +uv venv dashbls + +# Enter venv +source dashbls/bin/activate + +# Install developer dependencies +uv pip install -e ".[dev]" + +# Build binds +uv build + +# Run linter and formatter +uv run ruff check +uv run ruff format --check + +# Run unit tests +uv run python binds/python/test.py + +# Run benchmarks +uv run python binds/python/benchmark.py ``` ## License diff --git a/binds/python/benchmark.py b/binds/python/benchmark.py old mode 100644 new mode 100755 index 8ba3b7c0a..52647fc57 --- a/binds/python/benchmark.py +++ b/binds/python/benchmark.py @@ -1,6 +1,6 @@ -# flake8: noqa: E501 -import time import secrets +import sys +import time from dashbls import ( AugSchemeMPL, @@ -9,18 +9,23 @@ PrivateKey, ) -def startStopwatch(): + +def startStopwatch() -> float: return time.perf_counter() -def endStopwatch(test_name, start, numIters): + +def endStopwatch(test_name: str, start: float, numIters: int) -> None: end_time = time.perf_counter() duration = end_time - start - print("\n%s\nTotal: %d runs in %0.1f ms\nAvg: %f" - % (test_name, numIters, duration * 1000, duration * 1000 / numIters)) + print( + f"\n{test_name}\nTotal: {numIters} runs in {duration * 1000:.1f} ms\n" + f"Avg: {duration * 1000 / numIters:f}" + ) -def batch_verification(): + +def batch_verification() -> None: numIters = 100000 sig_bytes = [] @@ -39,28 +44,29 @@ def batch_verification(): pks = [] - start = startStopwatch(); + start = startStopwatch() for pk in pk_bytes: pks.append(G1Element.from_bytes(pk)) - endStopwatch("Public key validation", start, numIters); + endStopwatch("Public key validation", start, numIters) sigs = [] start = startStopwatch() for sig in sig_bytes: sigs.append(G2Element.from_bytes(sig)) - endStopwatch("Signature validation", start, numIters); + endStopwatch("Signature validation", start, numIters) start = startStopwatch() aggSig = AugSchemeMPL.aggregate(sigs) - endStopwatch("Aggregation", start, numIters); + endStopwatch("Aggregation", start, numIters) start = startStopwatch() - ok = AugSchemeMPL.aggregate_verify(pks, ms, aggSig); - endStopwatch("Batch verification", start, numIters); + ok = AugSchemeMPL.aggregate_verify(pks, ms, aggSig) + endStopwatch("Batch verification", start, numIters) if not ok: print("aggregate_verification failed!") sys.exit(1) + batch_verification() diff --git a/binds/python/test.py b/binds/python/test.py old mode 100644 new mode 100755 index 0c2fdf172..46781658a --- a/binds/python/test.py +++ b/binds/python/test.py @@ -1,7 +1,7 @@ # flake8: noqa: E501 import binascii +import contextlib import time -from copy import deepcopy from dashbls import ( AugSchemeMPL, @@ -10,11 +10,10 @@ G2Element, PopSchemeMPL, PrivateKey, - Util, ) -def test_schemes(): +def test_schemes() -> None: # fmt: off seed = bytes([ 0, 50, 6, 244, 24, 199, 1, 25, 52, 88, 192, 19, 18, 12, 89, 6, @@ -86,7 +85,7 @@ def test_schemes(): assert Scheme.verify(childUPk, msg, sigU_child) -def test_vectors_invalid(): +def test_vectors_invalid() -> None: # Invalid inputs from https://github.com/algorand/bls_sigs_ref/blob/master/python-impl/serdesZ.py invalid_inputs_1 = [ # infinity points: too short @@ -127,22 +126,18 @@ def test_vectors_invalid(): for s in invalid_inputs_1: bytes_ = binascii.unhexlify(s) - try: - g1 = G1Element(bytes_) - assert False, "Failed to disallow creation of G1 element." - except Exception as e: - pass + with contextlib.suppress(ValueError): + G1Element(bytes_) + raise AssertionError("Failed to disallow creation of G1 element.") for s in invalid_inputs_2: bytes_ = binascii.unhexlify(s) - try: - g2 = G2Element(bytes_) - assert False, "Failed to disallow creation of G2 element." - except Exception as e: - pass + with contextlib.suppress(ValueError): + G2Element(bytes_) + raise AssertionError("Failed to disallow creation of G2 element.") -def test_vectors_valid(): +def test_vectors_valid() -> None: # The following code was used to generate these vectors """ from py_ecc.bls import ( @@ -205,7 +200,7 @@ def test_vectors_valid(): assert bytes(sigAPop) == ref_sigAPop -def test_readme(): +def test_readme() -> None: seed: bytes = bytes( [ 0, @@ -319,7 +314,7 @@ def test_readme(): master_sk: PrivateKey = AugSchemeMPL.key_gen(seed) child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) - grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) + _grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) master_pk: G1Element = master_sk.get_g1() child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) @@ -332,42 +327,37 @@ def test_readme(): assert ok -def test_aggregate_verify_zero_items(): +def test_aggregate_verify_zero_items() -> None: assert AugSchemeMPL.aggregate_verify([], [], G2Element()) -def test_invalid_points(): - sk1 = BasicSchemeMPL.key_gen(b"1" *32) +def test_invalid_points() -> None: + sk1 = BasicSchemeMPL.key_gen(b"1" * 32) good_point = sk1.get_g1() good_point_bytes = bytes(good_point) start = time.time() - for i in range(2000): + for _i in range(2000): gp1 = G1Element.from_bytes(good_point_bytes) - print(f"from_bytes avg: {(time.time() - start) }") + print(f"from_bytes avg: {(time.time() - start)}") start = time.time() - for i in range(2000): + for _i in range(2000): gp2 = G1Element.from_bytes_unchecked(good_point_bytes) - print(f"from_bytes_unchecked avg: {(time.time() - start) }") + print(f"from_bytes_unchecked avg: {(time.time() - start)}") assert gp1 == gp2 - bad_point_hex: str = "8d5d0fb73b9c92df4eab4216e48c3e358578b4cc30f82c268bd6fef3bd34b558628daf1afef798d4c3b0fcd8b28c8973"; - try: + bad_point_hex: str = "8d5d0fb73b9c92df4eab4216e48c3e358578b4cc30f82c268bd6fef3bd34b558628daf1afef798d4c3b0fcd8b28c8973" + with contextlib.suppress(ValueError): G1Element.from_bytes(bytes.fromhex(bad_point_hex)) - assert False - except ValueError: - pass + raise AssertionError - p: G1Element = G1Element.from_bytes_unchecked(bytes.fromhex(bad_point_hex)) + _p: G1Element = G1Element.from_bytes_unchecked(bytes.fromhex(bad_point_hex)) bad_g2_point_hex = "8f2886c94eaeac335c8414cbf14c16681b225380cfee3293becc4531d5b415984b4ea4050d9ecda11fbc21c60627e9d212dfcb17d2b5ae399aa3fbcb099e05baa496b852ad976fb633cc6766b02fca4da549dc063908463b2906ad64e8b310ad" - try: + with contextlib.suppress(ValueError): G2Element.from_bytes(bytes.fromhex(bad_g2_point_hex)) - assert False - except ValueError: - pass - + raise AssertionError test_schemes() diff --git a/pyproject.toml b/pyproject.toml index ef93bef01..85da3e2dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,43 @@ Homepage = "https://github.com/dashpay/bls-signatures" Source = "https://github.com/dashpay/bls-signatures" Issues = "https://github.com/dashpay/bls-signatures/issues" +[project.optional-dependencies] +dev = [ + "ruff>=0.9", +] + [build-system] requires = ["setuptools>=77", "setuptools_scm>=8", "pybind11>=2.13.6"] build-backend = "setuptools.build_meta" [tool.setuptools_scm] local_scheme = "no-local-version" + +[tool.ruff] +indent-width = 4 +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "ANN", # flake8-annotations + "B", # flake8-bugbear + "BLE", # flake8-blind-except + "E", # pycodestyle errors + "F", # pyflakes + "I", # isort + "RUF", # ruff + "S", # flake8-bandit + "TC", # flake8-type-checking + "UP", # pyupgrade + "W", # pycodestyle warnings +] + +[tool.ruff.lint.per-file-ignores] +"binds/python/test.py" = ["S101"] + +[tool.ruff.format] +indent-style = "space" +line-ending = "lf" +quote-style = "double" +skip-magic-trailing-comma = false diff --git a/setup.py b/setup.py index 7212aa95c..f695bd26f 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ import os import re +import shutil import subprocess import sys import sysconfig @@ -21,20 +22,33 @@ class CMakeExtension(Extension): - def __init__(self, name, sourcedir=""): + def __init__(self, name: str, sourcedir: str = "") -> None: super().__init__(name, sources=[]) self.sourcedir = os.path.abspath(sourcedir) class CMakeBuild(build_ext): CMAKE_MINIMUM = (3, 18, 0) - WINDOWS_GENERATOR_ARCH = {"win-amd64": "x64", "win-arm64": "ARM64", "win32": "Win32"} + WINDOWS_GENERATOR_ARCH = ( + ("win-amd64", "x64"), + ("win-arm64", "ARM64"), + ("win32", "Win32"), + ) + + def _cmake(self) -> str: + cmake = shutil.which("cmake") + if cmake is None: + raise RuntimeError("CMake must be installed to build dashbls") + return cmake - def run(self): + def run(self) -> None: + cmake = self._cmake() try: - out = subprocess.check_output(["cmake", "--version"]).decode() - except OSError: - raise RuntimeError("CMake must be installed to build dashbls") + out = subprocess.check_output( # noqa: S603 + [cmake, "--version"], encoding="utf-8", errors="replace" + ) + except (OSError, subprocess.CalledProcessError) as error: + raise RuntimeError("cannot run cmake --version") from error found = re.search(r"version\s*([\d.]+)", out) if found is None: raise RuntimeError("cannot read a version out of: " + out.strip()) @@ -45,40 +59,40 @@ def run(self): if version < self.CMAKE_MINIMUM: raise RuntimeError( "CMake >= {} is required, found {}".format( - ".".join(str(p) for p in self.CMAKE_MINIMUM), found.group(1) + ".".join(str(p) for p in self.CMAKE_MINIMUM), + found.group(1), ) ) for ext in self.extensions: self.build_extension(ext) - def build_extension(self, ext): + def build_extension(self, ext: CMakeExtension) -> None: extdir = os.path.abspath(os.path.dirname(self.get_ext_fullpath(ext.name))) cmake_args = [ "-DBUILD_BLS_BENCHMARKS=OFF", "-DBUILD_BLS_TESTS=OFF", "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY=" + extdir, "-DMULTI=", - "-DPYTHON_EXECUTABLE=" + sys.executable, - "-DPYTHON_EXTENSION_SUFFIX=" + (sysconfig.get_config_var("EXT_SUFFIX") or ""), + "-DPYBIND11_FINDPYTHON=ON", + "-DPython_EXECUTABLE=" + sys.executable, ] try: import pybind11 - cmake_args.append("-Dpybind11_DIR=" + pybind11.get_cmake_dir()) except ImportError: pass + else: + cmake_args.append("-Dpybind11_DIR=" + pybind11.get_cmake_dir()) cfg = "Debug" if self.debug else "Release" build_args = ["--config", cfg, "--parallel", str(os.cpu_count() or 1)] if sys.platform == "win32": target = sysconfig.get_platform() - arch = self.WINDOWS_GENERATOR_ARCH.get(target) + arch = dict(self.WINDOWS_GENERATOR_ARCH).get(target) if arch is None: raise RuntimeError("unsupported windows platform: " + target) - cmake_args.append( - "-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{}={}".format(cfg.upper(), extdir) - ) + cmake_args.append(f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_{cfg.upper()}={extdir}") generator = os.environ.get("CMAKE_GENERATOR", "Visual Studio") if generator.startswith("Visual Studio"): cmake_args += ["-A", arch] @@ -88,21 +102,21 @@ def build_extension(self, ext): else: cmake_args += ["-DCMAKE_BUILD_TYPE=" + cfg] - env = os.environ.copy() - env["CXXFLAGS"] = '{} -DVERSION_INFO=\\"{}\\"'.format( - env.get("CXXFLAGS", ""), self.distribution.get_version() - ) os.makedirs(self.build_temp, exist_ok=True) - subprocess.check_call( - ["cmake", ext.sourcedir] + cmake_args, cwd=self.build_temp, env=env + cmake = self._cmake() + subprocess.check_call( # noqa: S603 + [cmake, ext.sourcedir, *cmake_args], cwd=self.build_temp ) - subprocess.check_call( - ["cmake", "--build", "."] + build_args, cwd=self.build_temp + subprocess.check_call( # noqa: S603 + [cmake, "--build", ".", *build_args], cwd=self.build_temp ) setup( - ext_modules=[CMakeExtension("dashbls", ".")], + package_dir={"": "binds/python"}, + packages=[], + py_modules=[], + ext_modules=[CMakeExtension("dashbls", os.path.dirname(os.path.abspath(__file__)))], cmdclass=dict(build_ext=CMakeBuild), zip_safe=False, ) From aec88bf9cbe9f38e9c9b4c51ff09b480b11e9c4e Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:30:41 +0530 Subject: [PATCH 17/31] build: add version awareness to python binds --- binds/python/CMakeLists.txt | 4 ++++ setup.py | 1 + 2 files changed, 5 insertions(+) diff --git a/binds/python/CMakeLists.txt b/binds/python/CMakeLists.txt index d05b7b649..98a776e4b 100644 --- a/binds/python/CMakeLists.txt +++ b/binds/python/CMakeLists.txt @@ -18,3 +18,7 @@ target_include_directories(dashbls_py PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) target_link_libraries(dashbls_py PRIVATE dashbls) + +if(VERSION_INFO) + target_compile_definitions(dashbls_py PRIVATE VERSION_INFO="${VERSION_INFO}") +endif() diff --git a/setup.py b/setup.py index f695bd26f..382b4647c 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ def build_extension(self, ext: CMakeExtension) -> None: "-DMULTI=", "-DPYBIND11_FINDPYTHON=ON", "-DPython_EXECUTABLE=" + sys.executable, + "-DVERSION_INFO=" + self.distribution.get_version(), ] try: From b3d42520caebd26b78e03badb6b6855233a3295c Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 01:50:29 +0530 Subject: [PATCH 18/31] refactor: switch python binds unit tests to `pytest` --- README.md | 3 +- binds/python/test.py | 383 -------------------------------------- binds/python/test_unit.py | 359 +++++++++++++++++++++++++++++++++++ pyproject.toml | 3 +- 4 files changed, 363 insertions(+), 385 deletions(-) delete mode 100755 binds/python/test.py create mode 100755 binds/python/test_unit.py diff --git a/README.md b/README.md index 0ec021a6b..c36bb2f73 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ cmake --build . --parallel 4 Our Python binds target Python 3.10 or higher; they depend on * [`pybind11`](https://github.com/pybind/pybind11) (bridging C++ and Python) +* [`pytest`](https://github.com/pytest-dev/pytest) (unit tests, part of optional `[.dev]` dependency group) * [`ruff`](https://github.com/astral-sh/ruff) (linting, part of optional `[.dev]` dependency group) > [!NOTE] @@ -78,7 +79,7 @@ uv run ruff check uv run ruff format --check # Run unit tests -uv run python binds/python/test.py +uv run pytest -v binds/python/test_unit.py # Run benchmarks uv run python binds/python/benchmark.py diff --git a/binds/python/test.py b/binds/python/test.py deleted file mode 100755 index 46781658a..000000000 --- a/binds/python/test.py +++ /dev/null @@ -1,383 +0,0 @@ -# flake8: noqa: E501 -import binascii -import contextlib -import time - -from dashbls import ( - AugSchemeMPL, - BasicSchemeMPL, - G1Element, - G2Element, - PopSchemeMPL, - PrivateKey, -) - - -def test_schemes() -> None: - # fmt: off - seed = bytes([ - 0, 50, 6, 244, 24, 199, 1, 25, 52, 88, 192, 19, 18, 12, 89, 6, - 220, 18, 102, 58, 209, 82, 12, 62, 89, 110, 182, 9, 44, 20, 254, 22 - ]) - # fmt: on - msg = bytes([100, 2, 254, 88, 90, 45, 23]) - msg2 = bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - sk = BasicSchemeMPL.key_gen(seed) - pk = sk.get_g1() - - assert sk == PrivateKey.from_bytes(bytes(sk)) - assert pk == G1Element.from_bytes(bytes(pk)) - - for Scheme in (BasicSchemeMPL, AugSchemeMPL, PopSchemeMPL): - sig = Scheme.sign(sk, msg) - assert sig == G2Element.from_bytes(bytes(sig)) - assert Scheme.verify(pk, msg, sig) - - seed = bytes([1]) + seed[1:] - sk1 = BasicSchemeMPL.key_gen(seed) - pk1 = sk1.get_g1() - seed = bytes([2]) + seed[1:] - sk2 = BasicSchemeMPL.key_gen(seed) - pk2 = sk2.get_g1() - - for Scheme in (BasicSchemeMPL, AugSchemeMPL, PopSchemeMPL): - # Aggregate same message - agg_pk = pk1 + pk2 - if Scheme is AugSchemeMPL: - sig1 = Scheme.sign(sk1, msg, agg_pk) - sig2 = Scheme.sign(sk2, msg, agg_pk) - else: - sig1 = Scheme.sign(sk1, msg) - sig2 = Scheme.sign(sk2, msg) - agg_sig = Scheme.aggregate([sig1, sig2]) - - assert Scheme.verify(agg_pk, msg, agg_sig) - - # Aggregate different message - sig1 = Scheme.sign(sk1, msg) - sig2 = Scheme.sign(sk2, msg2) - agg_sig = Scheme.aggregate([sig1, sig2]) - assert Scheme.aggregate_verify([pk1, pk2], [msg, msg2], agg_sig) - - # Manual pairing calculation and verification - if Scheme is AugSchemeMPL: - # AugSchemeMPL requires prepending the public key to message - aug_msg1 = bytes(pk1) + msg - aug_msg2 = bytes(pk2) + msg2 - else: - aug_msg1 = msg - aug_msg2 = msg2 - pair1 = pk1.pair(Scheme.g2_from_message(aug_msg1)) - pair2 = pk2.pair(Scheme.g2_from_message(aug_msg2)) - pair = pair1 * pair2 - agg_sig_pair = G1Element.generator().pair(agg_sig) - assert pair == agg_sig_pair - - # HD keys - child = Scheme.derive_child_sk(sk1, 123) - childU = Scheme.derive_child_sk_unhardened(sk1, 123) - childUPk = Scheme.derive_child_pk_unhardened(pk1, 123) - - sig_child = Scheme.sign(child, msg) - assert Scheme.verify(child.get_g1(), msg, sig_child) - - sigU_child = Scheme.sign(childU, msg) - assert Scheme.verify(childUPk, msg, sigU_child) - - -def test_vectors_invalid() -> None: - # Invalid inputs from https://github.com/algorand/bls_sigs_ref/blob/master/python-impl/serdesZ.py - invalid_inputs_1 = [ - # infinity points: too short - "c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # infinity points: not all zeros - "c00000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000", - # bad tags - "3a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - "7a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - "fa0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - # wrong length for compresed point - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa", - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaaaa", - # invalid x-coord - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", - # invalid elm of Fp --- equal to p (must be strictly less) - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", - ] - invalid_inputs_2 = [ - # infinity points: too short - "c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # infinity points: not all zeros - "c00000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000", - # bad tags - "3a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "7a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "fa0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # wrong length for compressed point - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - # invalid x-coord - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaa7", - # invalid elm of Fp --- equal to p (must be strictly less) - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", - ] - - for s in invalid_inputs_1: - bytes_ = binascii.unhexlify(s) - with contextlib.suppress(ValueError): - G1Element(bytes_) - raise AssertionError("Failed to disallow creation of G1 element.") - - for s in invalid_inputs_2: - bytes_ = binascii.unhexlify(s) - with contextlib.suppress(ValueError): - G2Element(bytes_) - raise AssertionError("Failed to disallow creation of G2 element.") - - -def test_vectors_valid() -> None: - # The following code was used to generate these vectors - """ - from py_ecc.bls import ( - G2Basic, - G2MessageAugmentation as G2MA, - G2ProofOfPossession as G2Pop, - ) - - secret1 = bytes([1] * 32) - secret2 = bytes([x * 314159 % 256 for x in range(32)]) - sk1 = int.from_bytes(secret1, 'big') - sk2 = int.from_bytes(secret2, 'big') - msg = bytes([3, 1, 4, 1, 5, 9]) - pk1 = G2Basic.SkToPk(sk1) - pk2 = G2Basic.SkToPk(sk2) - - for Scheme in (G2Basic, G2MA, G2Pop): - sig1 = Scheme.Sign(sk1, msg) - sig2 = Scheme.Sign(sk2, msg) - sig_agg = Scheme.Aggregate([sig1, sig2]) - print(sig1) - print(sig2) - print(sig_agg) - """ - - ref_sig1Basic = b"\x96\xba4\xfa\xc3<\x7f\x12\x9d`*\x0b\xc8\xa3\xd4?\x9a\xbc\x01N\xce\xaa\xb75\x91F\xb4\xb1P\xe5{\x80\x86Es\x8f5g\x1e\x9e\x10\xe0\xd8b\xa3\x0c\xabp\x07N\xb5\x83\x1d\x13\xe6\xa5\xb1b\xd0\x1e\xeb\xe6\x87\xd0\x16J\xdb\xd0\xa8d7\n|\"*'h\xd7pM\xa2T\xf1\xbf\x18#f[\xc26\x1f\x9d\xd8\xc0\x0e\x99" - ref_sig2Basic = b'\xa4\x02y\t2\x13\x0fvj\xf1\x1b\xa7\x16Sf\x83\xd8\xc4\xcf\xa5\x19G\xe4\xf9\x08\x1f\xed\xd6\x92\xd6\xdc\x0c\xac[\x90K\xee^\xa6\xe2Ui\xe3m{\xe4\xcaY\x06\x9a\x96\xe3K\x7fp\x07X\xb7\x16\xf9IJ\xaaY\xa9nt\xd1J;U*\x9ak\xc1)\xe7\x17\x19[\x9d`\x06\xfdm\\\xefGh\xc0"\xe0\xf71j\xbf' - ref_sigABasic = b"\x98|\xfd;\xcdb(\x02\x87\x02t\x83\xf2\x9cU$^\xd81\xf5\x1d\xd6\xbd\x99\x9ao\xf1\xa1\xf1\xf1\xf0\xb6Gw\x8b\x01g5\x9cqPUX\xa7n\x15\x8ef\x18\x1e\xe5\x12Y\x05\xa6B$k\x01\xe7\xfa^\xe5=h\xa4\xfe\x9b\xfb)\xa8\xe2f\x01\xf0\xb9\xadW}\xdd\x18\x87js1|!n\xa6\x1fC\x04\x14\xecQ\xc5" - ref_sig1Aug = b'\x81\x80\xf0,\xcbr\xe9"\xb1R\xfc\xed\xbe\x0e\x1d\x19R\x105Opp6X\xe8\xe0\x8c\xbe\xbf\x11\xd4\x97\x0e\xabj\xc3\xcc\xf7\x15\xf3\xfb\x87m\xf9\xa9yz\xbd\x0c\x1a\xf6\x1a\xae\xad\xc9,,\xfe\\\nV\xc1F\xcc\x8c?qQ\xa0s\xcf_\x16\xdf8$g$\xc4\xae\xd7?\xf3\x0e\xf5\xda\xa6\xaa\xca\xed\x1a&\xec\xaa3k' - ref_sig2Aug = b'\x99\x11\x1e\xea\xfbA-\xa6\x1eL7\xd3\xe8\x06\xc6\xfdj\xc9\xf3\x87\x0eT\xda\x92"\xbaNIH"\xc5\xb7eg1\xfazdY4\xd0KU\x9e\x92a\xb8b\x01\xbb\xeeW\x05RP\xa4Y\xa2\xda\x10\xe5\x1f\x9c\x1aiA)\x7f\xfc]\x97\nUr6\xd0\xbd\xeb|\xf8\xff\x18\x80\x0b\x08c8q\xa0\xf0\xa7\xeaB\xf4t\x80' - ref_sigAAug = b"\x8c]\x03\xf9\xda\xe7~\x19\xa5\x94Z\x06\xa2\x14\x83n\xdb\x8e\x03\xb8QR]\x84\xb9\xded@\xe6\x8f\xc0\xcas\x03\xee\xed9\r\x86<\x9bU\xa8\xcfmY\x14\n\x01\xb5\x88G\x88\x1e\xb5\xafgsMD\xb2UVF\xc6al9\xab\x88\xd2S)\x9a\xcc\x1e\xb1\xb1\x9d\xdb\x9b\xfc\xbev\xe2\x8a\xdd\xf6q\xd1\x16\xc0R\xbb\x18G" - ref_sig1Pop = b"\x95P\xfbN\x7f~\x8c\xc4\xa9\x0b\xe8V\n\xb5\xa7\x98\xb0\xb20\x00\xb6\xa5J!\x17R\x02\x10\xf9\x86\xf3\xf2\x81\xb3v\xf2Y\xc0\xb7\x80b\xd1\xeb1\x92\xb3\xd9\xbb\x04\x9fY\xec\xc1\xb0:pI\xebf^\r\xf3d\x94\xaeL\xb5\xf1\x13l\xca\xee\xfc\x99X\xcb0\xc33==C\xf0qH\xc3\x86)\x9a{\x1b\xfc\r\xc5\xcf|" - ref_sig2Pop = b"\xa6\x906\xbc\x11\xae^\xfc\xbfa\x80\xaf\xe3\x9a\xdd\xde~'s\x1e\xc4\x02W\xbf\xdc<7\xf1{\x8d\xf6\x83\x06\xa3N\xbd\x10\xe9\xe3*5%7P\xdf\\\x87\xc2\x14/\x82\x07\xe8\xd5eG\x12\xb4\xe5T\xf5\x85\xfbhF\xff8\x04\xe4)\xa9\xf8\xa1\xb4\xc5ku\xd0\x86\x9e\xd6u\x80\xd7\x89\x87\x0b\xab\xe2\xc7\xc8\xa9\xd5\x1e{*" - ref_sigAPop = b"\xa4\xeat+\xcd\xc1U>\x9c\xa4\xe5`\xbe~^ln\xfajd\xdd\xdf\x9c\xa3\xbb(T#=\x85\xa6\xaa\xc1\xb7n\xc7\xd1\x03\xdbN3\x14\x8b\x82\xaf\x99#\xdb\x05\x93Jn\xce\x9aq\x01\xcd\x8a\x9dG\xce'\x97\x80V\xb0\xf5\x90\x00!\x81\x8cEi\x8a\xfd\xd6\xcf\x8ako\x7f\xee\x1f\x0bCqoU\xe4\x13\xd4\xb8z`9" - - secret1 = bytes([1] * 32) - secret2 = bytes([x * 314159 % 256 for x in range(32)]) - sk1 = PrivateKey.from_bytes(secret1) - sk2 = PrivateKey.from_bytes(secret2) - - msg = bytes([3, 1, 4, 1, 5, 9]) - sig1Basic = BasicSchemeMPL.sign(sk1, msg) - sig2Basic = BasicSchemeMPL.sign(sk2, msg) - sigABasic = BasicSchemeMPL.aggregate([sig1Basic, sig2Basic]) - sig1Aug = AugSchemeMPL.sign(sk1, msg) - sig2Aug = AugSchemeMPL.sign(sk2, msg) - sigAAug = AugSchemeMPL.aggregate([sig1Aug, sig2Aug]) - sig1Pop = PopSchemeMPL.sign(sk1, msg) - sig2Pop = PopSchemeMPL.sign(sk2, msg) - sigAPop = PopSchemeMPL.aggregate([sig1Pop, sig2Pop]) - - assert bytes(sig1Basic) == ref_sig1Basic - assert bytes(sig2Basic) == ref_sig2Basic - assert bytes(sigABasic) == ref_sigABasic - assert bytes(sig1Aug) == ref_sig1Aug - assert bytes(sig2Aug) == ref_sig2Aug - assert bytes(sigAAug) == ref_sigAAug - assert bytes(sig1Pop) == ref_sig1Pop - assert bytes(sig2Pop) == ref_sig2Pop - assert bytes(sigAPop) == ref_sigAPop - - -def test_readme() -> None: - seed: bytes = bytes( - [ - 0, - 50, - 6, - 244, - 24, - 199, - 1, - 25, - 52, - 88, - 192, - 19, - 18, - 12, - 89, - 6, - 220, - 18, - 102, - 58, - 209, - 82, - 12, - 62, - 89, - 110, - 182, - 9, - 44, - 20, - 254, - 22, - ] - ) - sk: PrivateKey = AugSchemeMPL.key_gen(seed) - pk: G1Element = sk.get_g1() - - message: bytes = bytes([1, 2, 3, 4, 5]) - signature: G2Element = AugSchemeMPL.sign(sk, message) - - ok: bool = AugSchemeMPL.verify(pk, message, signature) - assert ok - - sk_bytes: bytes = bytes(sk) # 32 bytes - pk_bytes: bytes = bytes(pk) # 48 bytes - signature_bytes: bytes = bytes(signature) # 96 bytes - - print(sk_bytes.hex(), pk_bytes.hex(), signature_bytes.hex()) - - sk = PrivateKey.from_bytes(sk_bytes) - pk = G1Element.from_bytes(pk_bytes) - signature = G2Element.from_bytes(signature_bytes) - - seed = bytes([1]) + seed[1:] - sk1: PrivateKey = AugSchemeMPL.key_gen(seed) - seed = bytes([2]) + seed[1:] - sk2: PrivateKey = AugSchemeMPL.key_gen(seed) - message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) - - pk1: G1Element = sk1.get_g1() - sig1: G2Element = AugSchemeMPL.sign(sk1, message) - - pk2: G1Element = sk2.get_g1() - sig2: G2Element = AugSchemeMPL.sign(sk2, message2) - - agg_sig: G2Element = AugSchemeMPL.aggregate([sig1, sig2]) - - ok = AugSchemeMPL.aggregate_verify([pk1, pk2], [message, message2], agg_sig) - assert ok - - seed = bytes([3]) + seed[1:] - sk3: PrivateKey = AugSchemeMPL.key_gen(seed) - pk3: G1Element = sk3.get_g1() - message3: bytes = bytes([100, 2, 254, 88, 90, 45, 23]) - sig3: G2Element = AugSchemeMPL.sign(sk3, message3) - - agg_sig_final: G2Element = AugSchemeMPL.aggregate([agg_sig, sig3]) - ok = AugSchemeMPL.aggregate_verify( - [pk1, pk2, pk3], [message, message2, message3], agg_sig_final - ) - assert ok - - pop_sig1: G2Element = PopSchemeMPL.sign(sk1, message) - pop_sig2: G2Element = PopSchemeMPL.sign(sk2, message) - pop_sig3: G2Element = PopSchemeMPL.sign(sk3, message) - pop1: G2Element = PopSchemeMPL.pop_prove(sk1) - pop2: G2Element = PopSchemeMPL.pop_prove(sk2) - pop3: G2Element = PopSchemeMPL.pop_prove(sk3) - - ok = PopSchemeMPL.pop_verify(pk1, pop1) - assert ok - ok = PopSchemeMPL.pop_verify(pk2, pop2) - assert ok - ok = PopSchemeMPL.pop_verify(pk3, pop3) - assert ok - - pop_sig_agg: G2Element = PopSchemeMPL.aggregate([pop_sig1, pop_sig2, pop_sig3]) - - ok = PopSchemeMPL.fast_aggregate_verify([pk1, pk2, pk3], message, pop_sig_agg) - assert ok - - pop_agg_pk: G1Element = pk1 + pk2 + pk3 - ok = PopSchemeMPL.verify(pop_agg_pk, message, pop_sig_agg) - assert ok - - pop_agg_sk: PrivateKey = PrivateKey.aggregate([sk1, sk2, sk3]) - ok = PopSchemeMPL.sign(pop_agg_sk, message) == pop_sig_agg - assert ok - - master_sk: PrivateKey = AugSchemeMPL.key_gen(seed) - child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) - _grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) - - master_pk: G1Element = master_sk.get_g1() - child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) - grandchild_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(child_u, 0) - - child_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(master_pk, 22) - grandchild_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(child_u_pk, 0) - - ok = grandchild_u_pk == grandchild_u.get_g1() - assert ok - - -def test_aggregate_verify_zero_items() -> None: - assert AugSchemeMPL.aggregate_verify([], [], G2Element()) - - -def test_invalid_points() -> None: - sk1 = BasicSchemeMPL.key_gen(b"1" * 32) - good_point = sk1.get_g1() - good_point_bytes = bytes(good_point) - start = time.time() - for _i in range(2000): - gp1 = G1Element.from_bytes(good_point_bytes) - print(f"from_bytes avg: {(time.time() - start)}") - - start = time.time() - for _i in range(2000): - gp2 = G1Element.from_bytes_unchecked(good_point_bytes) - print(f"from_bytes_unchecked avg: {(time.time() - start)}") - assert gp1 == gp2 - - bad_point_hex: str = "8d5d0fb73b9c92df4eab4216e48c3e358578b4cc30f82c268bd6fef3bd34b558628daf1afef798d4c3b0fcd8b28c8973" - with contextlib.suppress(ValueError): - G1Element.from_bytes(bytes.fromhex(bad_point_hex)) - raise AssertionError - - _p: G1Element = G1Element.from_bytes_unchecked(bytes.fromhex(bad_point_hex)) - - bad_g2_point_hex = "8f2886c94eaeac335c8414cbf14c16681b225380cfee3293becc4531d5b415984b4ea4050d9ecda11fbc21c60627e9d212dfcb17d2b5ae399aa3fbcb099e05baa496b852ad976fb633cc6766b02fca4da549dc063908463b2906ad64e8b310ad" - - with contextlib.suppress(ValueError): - G2Element.from_bytes(bytes.fromhex(bad_g2_point_hex)) - raise AssertionError - - -test_schemes() -test_vectors_invalid() -test_vectors_valid() -test_readme() -test_aggregate_verify_zero_items() -test_invalid_points() - -print("\nAll tests passed.") - -""" -Copyright 2020 Chia Network Inc -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py new file mode 100755 index 000000000..ff0bcbb21 --- /dev/null +++ b/binds/python/test_unit.py @@ -0,0 +1,359 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Unit tests for the dashbls package.""" + +import binascii + +import pytest +from dashbls import ( + AugSchemeMPL, + BasicSchemeMPL, + G1Element, + G2Element, + PopSchemeMPL, + PrivateKey, +) + +# fmt: off +SEED = bytes([ + 0, 50, 6, 244, 24, 199, 1, 25, 52, 88, 192, 19, 18, 12, 89, 6, + 220, 18, 102, 58, 209, 82, 12, 62, 89, 110, 182, 9, 44, 20, 254, 22 +]) +# fmt: on +MSG = bytes([100, 2, 254, 88, 90, 45, 23]) +MSG2 = bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) + +SCHEMES = (BasicSchemeMPL, AugSchemeMPL, PopSchemeMPL) +SCHEME_IDS = [scheme.__name__ for scheme in SCHEMES] + + +def _derive_two_keypairs() -> tuple[PrivateKey, G1Element, PrivateKey, G1Element]: + seed1 = bytes([1]) + SEED[1:] + sk1 = BasicSchemeMPL.key_gen(seed1) + seed2 = bytes([2]) + seed1[1:] + sk2 = BasicSchemeMPL.key_gen(seed2) + return sk1, sk1.get_g1(), sk2, sk2.get_g1() + + +@pytest.fixture +def keypairs() -> tuple[PrivateKey, G1Element, PrivateKey, G1Element]: + return _derive_two_keypairs() + + +def test_private_key_and_public_key_roundtrip() -> None: + sk = BasicSchemeMPL.key_gen(SEED) + pk = sk.get_g1() + assert sk == PrivateKey.from_bytes(bytes(sk)) + assert pk == G1Element.from_bytes(bytes(pk)) + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_sign_and_verify_roundtrip(scheme: type) -> None: + sk = BasicSchemeMPL.key_gen(SEED) + pk = sk.get_g1() + sig = scheme.sign(sk, MSG) + assert sig == G2Element.from_bytes(bytes(sig)) + assert scheme.verify(pk, MSG, sig) + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_aggregate_verify_same_message( + scheme: type, keypairs: tuple[PrivateKey, G1Element, PrivateKey, G1Element] +) -> None: + sk1, pk1, sk2, pk2 = keypairs + agg_pk = pk1 + pk2 + if scheme is AugSchemeMPL: + sig1 = scheme.sign(sk1, MSG, agg_pk) + sig2 = scheme.sign(sk2, MSG, agg_pk) + else: + sig1 = scheme.sign(sk1, MSG) + sig2 = scheme.sign(sk2, MSG) + agg_sig = scheme.aggregate([sig1, sig2]) + assert scheme.verify(agg_pk, MSG, agg_sig) + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_aggregate_verify_different_messages( + scheme: type, keypairs: tuple[PrivateKey, G1Element, PrivateKey, G1Element] +) -> None: + sk1, pk1, sk2, pk2 = keypairs + sig1 = scheme.sign(sk1, MSG) + sig2 = scheme.sign(sk2, MSG2) + agg_sig = scheme.aggregate([sig1, sig2]) + assert scheme.aggregate_verify([pk1, pk2], [MSG, MSG2], agg_sig) + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_manual_pairing_matches_aggregate_signature( + scheme: type, keypairs: tuple[PrivateKey, G1Element, PrivateKey, G1Element] +) -> None: + sk1, pk1, sk2, pk2 = keypairs + sig1 = scheme.sign(sk1, MSG) + sig2 = scheme.sign(sk2, MSG2) + agg_sig = scheme.aggregate([sig1, sig2]) + + if scheme is AugSchemeMPL: + # AugSchemeMPL requires prepending the public key to the message + aug_msg1 = bytes(pk1) + MSG + aug_msg2 = bytes(pk2) + MSG2 + else: + aug_msg1 = MSG + aug_msg2 = MSG2 + pair1 = pk1.pair(scheme.g2_from_message(aug_msg1)) + pair2 = pk2.pair(scheme.g2_from_message(aug_msg2)) + pair = pair1 * pair2 + agg_sig_pair = G1Element.generator().pair(agg_sig) + assert pair == agg_sig_pair + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_hd_key_derivation_sign_and_verify( + scheme: type, keypairs: tuple[PrivateKey, G1Element, PrivateKey, G1Element] +) -> None: + sk1, pk1, _sk2, _pk2 = keypairs + child = scheme.derive_child_sk(sk1, 123) + child_u = scheme.derive_child_sk_unhardened(sk1, 123) + child_u_pk = scheme.derive_child_pk_unhardened(pk1, 123) + + sig_child = scheme.sign(child, MSG) + assert scheme.verify(child.get_g1(), MSG, sig_child) + + sig_u_child = scheme.sign(child_u, MSG) + assert scheme.verify(child_u_pk, MSG, sig_u_child) + + +# Invalid inputs from https://github.com/algorand/bls_sigs_ref/blob/master/python-impl/serdesZ.py +INVALID_G1_VECTORS = [ + # infinity points: too short + "c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + # infinity points: not all zeros + "c00000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000", + # bad tags + "3a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", + "7a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", + "fa0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", + # wrong length for compresed point + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaa", + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaaaa", + # invalid x-coord + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa", + # invalid elm of Fp --- equal to p (must be strictly less) + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", +] +INVALID_G2_VECTORS = [ + # infinity points: too short + "c000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + # infinity points: not all zeros + "c00000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000", + # bad tags + "3a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "7a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "fa0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + # wrong length for compressed point + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + # invalid x-coord + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaa7", + # invalid elm of Fp --- equal to p (must be strictly less) + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "9a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaaa1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", +] + + +@pytest.mark.parametrize("hex_str", INVALID_G1_VECTORS) +def test_invalid_g1_vector_rejected(hex_str: str) -> None: + with pytest.raises(ValueError): + G1Element(binascii.unhexlify(hex_str)) + + +@pytest.mark.parametrize("hex_str", INVALID_G2_VECTORS) +def test_invalid_g2_vector_rejected(hex_str: str) -> None: + with pytest.raises(ValueError): + G2Element(binascii.unhexlify(hex_str)) + + +# The following code was used to generate the reference vectors below +""" +from py_ecc.bls import ( + G2Basic, + G2MessageAugmentation as G2MA, + G2ProofOfPossession as G2Pop, +) + +secret1 = bytes([1] * 32) +secret2 = bytes([x * 314159 % 256 for x in range(32)]) +sk1 = int.from_bytes(secret1, 'big') +sk2 = int.from_bytes(secret2, 'big') +msg = bytes([3, 1, 4, 1, 5, 9]) +pk1 = G2Basic.SkToPk(sk1) +pk2 = G2Basic.SkToPk(sk2) + +for Scheme in (G2Basic, G2MA, G2Pop): + sig1 = Scheme.Sign(sk1, msg) + sig2 = Scheme.Sign(sk2, msg) + sig_agg = Scheme.Aggregate([sig1, sig2]) + print(sig1) + print(sig2) + print(sig_agg) +""" + +REFERENCE_VECTORS = { + BasicSchemeMPL: ( + b"\x96\xba4\xfa\xc3<\x7f\x12\x9d`*\x0b\xc8\xa3\xd4?\x9a\xbc\x01N\xce\xaa\xb75\x91F\xb4\xb1P\xe5{\x80\x86Es\x8f5g\x1e\x9e\x10\xe0\xd8b\xa3\x0c\xabp\x07N\xb5\x83\x1d\x13\xe6\xa5\xb1b\xd0\x1e\xeb\xe6\x87\xd0\x16J\xdb\xd0\xa8d7\n|\"*'h\xd7pM\xa2T\xf1\xbf\x18#f[\xc26\x1f\x9d\xd8\xc0\x0e\x99", + b'\xa4\x02y\t2\x13\x0fvj\xf1\x1b\xa7\x16Sf\x83\xd8\xc4\xcf\xa5\x19G\xe4\xf9\x08\x1f\xed\xd6\x92\xd6\xdc\x0c\xac[\x90K\xee^\xa6\xe2Ui\xe3m{\xe4\xcaY\x06\x9a\x96\xe3K\x7fp\x07X\xb7\x16\xf9IJ\xaaY\xa9nt\xd1J;U*\x9ak\xc1)\xe7\x17\x19[\x9d`\x06\xfdm\\\xefGh\xc0"\xe0\xf71j\xbf', + b"\x98|\xfd;\xcdb(\x02\x87\x02t\x83\xf2\x9cU$^\xd81\xf5\x1d\xd6\xbd\x99\x9ao\xf1\xa1\xf1\xf1\xf0\xb6Gw\x8b\x01g5\x9cqPUX\xa7n\x15\x8ef\x18\x1e\xe5\x12Y\x05\xa6B$k\x01\xe7\xfa^\xe5=h\xa4\xfe\x9b\xfb)\xa8\xe2f\x01\xf0\xb9\xadW}\xdd\x18\x87js1|!n\xa6\x1fC\x04\x14\xecQ\xc5", + ), + AugSchemeMPL: ( + b'\x81\x80\xf0,\xcbr\xe9"\xb1R\xfc\xed\xbe\x0e\x1d\x19R\x105Opp6X\xe8\xe0\x8c\xbe\xbf\x11\xd4\x97\x0e\xabj\xc3\xcc\xf7\x15\xf3\xfb\x87m\xf9\xa9yz\xbd\x0c\x1a\xf6\x1a\xae\xad\xc9,,\xfe\\\nV\xc1F\xcc\x8c?qQ\xa0s\xcf_\x16\xdf8$g$\xc4\xae\xd7?\xf3\x0e\xf5\xda\xa6\xaa\xca\xed\x1a&\xec\xaa3k', + b'\x99\x11\x1e\xea\xfbA-\xa6\x1eL7\xd3\xe8\x06\xc6\xfdj\xc9\xf3\x87\x0eT\xda\x92"\xbaNIH"\xc5\xb7eg1\xfazdY4\xd0KU\x9e\x92a\xb8b\x01\xbb\xeeW\x05RP\xa4Y\xa2\xda\x10\xe5\x1f\x9c\x1aiA)\x7f\xfc]\x97\nUr6\xd0\xbd\xeb|\xf8\xff\x18\x80\x0b\x08c8q\xa0\xf0\xa7\xeaB\xf4t\x80', + b"\x8c]\x03\xf9\xda\xe7~\x19\xa5\x94Z\x06\xa2\x14\x83n\xdb\x8e\x03\xb8QR]\x84\xb9\xded@\xe6\x8f\xc0\xcas\x03\xee\xed9\r\x86<\x9bU\xa8\xcfmY\x14\n\x01\xb5\x88G\x88\x1e\xb5\xafgsMD\xb2UVF\xc6al9\xab\x88\xd2S)\x9a\xcc\x1e\xb1\xb1\x9d\xdb\x9b\xfc\xbev\xe2\x8a\xdd\xf6q\xd1\x16\xc0R\xbb\x18G", + ), + PopSchemeMPL: ( + b"\x95P\xfbN\x7f~\x8c\xc4\xa9\x0b\xe8V\n\xb5\xa7\x98\xb0\xb20\x00\xb6\xa5J!\x17R\x02\x10\xf9\x86\xf3\xf2\x81\xb3v\xf2Y\xc0\xb7\x80b\xd1\xeb1\x92\xb3\xd9\xbb\x04\x9fY\xec\xc1\xb0:pI\xebf^\r\xf3d\x94\xaeL\xb5\xf1\x13l\xca\xee\xfc\x99X\xcb0\xc33==C\xf0qH\xc3\x86)\x9a{\x1b\xfc\r\xc5\xcf|", + b"\xa6\x906\xbc\x11\xae^\xfc\xbfa\x80\xaf\xe3\x9a\xdd\xde~'s\x1e\xc4\x02W\xbf\xdc<7\xf1{\x8d\xf6\x83\x06\xa3N\xbd\x10\xe9\xe3*5%7P\xdf\\\x87\xc2\x14/\x82\x07\xe8\xd5eG\x12\xb4\xe5T\xf5\x85\xfbhF\xff8\x04\xe4)\xa9\xf8\xa1\xb4\xc5ku\xd0\x86\x9e\xd6u\x80\xd7\x89\x87\x0b\xab\xe2\xc7\xc8\xa9\xd5\x1e{*", + b"\xa4\xeat+\xcd\xc1U>\x9c\xa4\xe5`\xbe~^ln\xfajd\xdd\xdf\x9c\xa3\xbb(T#=\x85\xa6\xaa\xc1\xb7n\xc7\xd1\x03\xdbN3\x14\x8b\x82\xaf\x99#\xdb\x05\x93Jn\xce\x9aq\x01\xcd\x8a\x9dG\xce'\x97\x80V\xb0\xf5\x90\x00!\x81\x8cEi\x8a\xfd\xd6\xcf\x8ako\x7f\xee\x1f\x0bCqoU\xe4\x13\xd4\xb8z`9", + ), +} + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_sign_matches_reference_vectors(scheme: type) -> None: + secret1 = bytes([1] * 32) + secret2 = bytes([x * 314159 % 256 for x in range(32)]) + sk1 = PrivateKey.from_bytes(secret1) + sk2 = PrivateKey.from_bytes(secret2) + msg = bytes([3, 1, 4, 1, 5, 9]) + + sig1 = scheme.sign(sk1, msg) + sig2 = scheme.sign(sk2, msg) + sig_agg = scheme.aggregate([sig1, sig2]) + + ref_sig1, ref_sig2, ref_sig_agg = REFERENCE_VECTORS[scheme] + assert bytes(sig1) == ref_sig1 + assert bytes(sig2) == ref_sig2 + assert bytes(sig_agg) == ref_sig_agg + + +def test_readme() -> None: + seed: bytes = SEED + sk: PrivateKey = AugSchemeMPL.key_gen(seed) + pk: G1Element = sk.get_g1() + + message: bytes = bytes([1, 2, 3, 4, 5]) + signature: G2Element = AugSchemeMPL.sign(sk, message) + + ok: bool = AugSchemeMPL.verify(pk, message, signature) + assert ok + + sk_bytes: bytes = bytes(sk) # 32 bytes + pk_bytes: bytes = bytes(pk) # 48 bytes + signature_bytes: bytes = bytes(signature) # 96 bytes + + sk = PrivateKey.from_bytes(sk_bytes) + pk = G1Element.from_bytes(pk_bytes) + signature = G2Element.from_bytes(signature_bytes) + + seed = bytes([1]) + seed[1:] + sk1: PrivateKey = AugSchemeMPL.key_gen(seed) + seed = bytes([2]) + seed[1:] + sk2: PrivateKey = AugSchemeMPL.key_gen(seed) + message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) + + pk1: G1Element = sk1.get_g1() + sig1: G2Element = AugSchemeMPL.sign(sk1, message) + + pk2: G1Element = sk2.get_g1() + sig2: G2Element = AugSchemeMPL.sign(sk2, message2) + + agg_sig: G2Element = AugSchemeMPL.aggregate([sig1, sig2]) + + ok = AugSchemeMPL.aggregate_verify([pk1, pk2], [message, message2], agg_sig) + assert ok + + seed = bytes([3]) + seed[1:] + sk3: PrivateKey = AugSchemeMPL.key_gen(seed) + pk3: G1Element = sk3.get_g1() + message3: bytes = bytes([100, 2, 254, 88, 90, 45, 23]) + sig3: G2Element = AugSchemeMPL.sign(sk3, message3) + + agg_sig_final: G2Element = AugSchemeMPL.aggregate([agg_sig, sig3]) + ok = AugSchemeMPL.aggregate_verify( + [pk1, pk2, pk3], [message, message2, message3], agg_sig_final + ) + assert ok + + pop_sig1: G2Element = PopSchemeMPL.sign(sk1, message) + pop_sig2: G2Element = PopSchemeMPL.sign(sk2, message) + pop_sig3: G2Element = PopSchemeMPL.sign(sk3, message) + pop1: G2Element = PopSchemeMPL.pop_prove(sk1) + pop2: G2Element = PopSchemeMPL.pop_prove(sk2) + pop3: G2Element = PopSchemeMPL.pop_prove(sk3) + + ok = PopSchemeMPL.pop_verify(pk1, pop1) + assert ok + ok = PopSchemeMPL.pop_verify(pk2, pop2) + assert ok + ok = PopSchemeMPL.pop_verify(pk3, pop3) + assert ok + + pop_sig_agg: G2Element = PopSchemeMPL.aggregate([pop_sig1, pop_sig2, pop_sig3]) + + ok = PopSchemeMPL.fast_aggregate_verify([pk1, pk2, pk3], message, pop_sig_agg) + assert ok + + pop_agg_pk: G1Element = pk1 + pk2 + pk3 + ok = PopSchemeMPL.verify(pop_agg_pk, message, pop_sig_agg) + assert ok + + pop_agg_sk: PrivateKey = PrivateKey.aggregate([sk1, sk2, sk3]) + ok = PopSchemeMPL.sign(pop_agg_sk, message) == pop_sig_agg + assert ok + + master_sk: PrivateKey = AugSchemeMPL.key_gen(seed) + child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) + _grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) + + master_pk: G1Element = master_sk.get_g1() + child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) + grandchild_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(child_u, 0) + + child_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(master_pk, 22) + grandchild_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(child_u_pk, 0) + + ok = grandchild_u_pk == grandchild_u.get_g1() + assert ok + + +def test_aggregate_verify_zero_items() -> None: + assert AugSchemeMPL.aggregate_verify([], [], G2Element()) + + +def test_from_bytes_and_from_bytes_unchecked_agree_on_valid_point() -> None: + sk1 = BasicSchemeMPL.key_gen(b"1" * 32) + good_point_bytes = bytes(sk1.get_g1()) + assert G1Element.from_bytes(good_point_bytes) == G1Element.from_bytes_unchecked( + good_point_bytes + ) + + +def test_from_bytes_rejects_invalid_g1_point() -> None: + bad_point_hex = "8d5d0fb73b9c92df4eab4216e48c3e358578b4cc30f82c268bd6fef3bd34b558628daf1afef798d4c3b0fcd8b28c8973" # noqa: E501 + with pytest.raises(ValueError): + G1Element.from_bytes(bytes.fromhex(bad_point_hex)) + # from_bytes_unchecked skips subgroup validation and must not raise + G1Element.from_bytes_unchecked(bytes.fromhex(bad_point_hex)) + + +def test_from_bytes_rejects_invalid_g2_point() -> None: + bad_g2_point_hex = "8f2886c94eaeac335c8414cbf14c16681b225380cfee3293becc4531d5b415984b4ea4050d9ecda11fbc21c60627e9d212dfcb17d2b5ae399aa3fbcb099e05baa496b852ad976fb633cc6766b02fca4da549dc063908463b2906ad64e8b310ad" # noqa: E501 + with pytest.raises(ValueError): + G2Element.from_bytes(bytes.fromhex(bad_g2_point_hex)) diff --git a/pyproject.toml b/pyproject.toml index 85da3e2dd..1fb350fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ Issues = "https://github.com/dashpay/bls-signatures/issues" [project.optional-dependencies] dev = [ "ruff>=0.9", + "pytest>=8.1", ] [build-system] @@ -47,7 +48,7 @@ select = [ ] [tool.ruff.lint.per-file-ignores] -"binds/python/test.py" = ["S101"] +"binds/python/test*.py" = ["S101"] [tool.ruff.format] indent-style = "space" From fabdcf2a95ca935a4a12e03c79aed8e20f70bc50 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:34:09 +0530 Subject: [PATCH 19/31] refactor: switch python binds benchmarks to `pytest-benchmark` --- README.md | 6 +-- binds/python/benchmark.py | 72 ------------------------------------ binds/python/test_bench.py | 75 ++++++++++++++++++++++++++++++++++++++ pyproject.toml | 5 +++ 4 files changed, 83 insertions(+), 75 deletions(-) delete mode 100755 binds/python/benchmark.py create mode 100755 binds/python/test_bench.py diff --git a/README.md b/README.md index c36bb2f73..dbb2b499b 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,8 @@ cmake --build . --parallel 4 Our Python binds target Python 3.10 or higher; they depend on * [`pybind11`](https://github.com/pybind/pybind11) (bridging C++ and Python) -* [`pytest`](https://github.com/pytest-dev/pytest) (unit tests, part of optional `[.dev]` dependency group) -* [`ruff`](https://github.com/astral-sh/ruff) (linting, part of optional `[.dev]` dependency group) +* [`pytest`](https://github.com/pytest-dev/pytest) (benchmarks and unit tests, part of optional `[dev]` dependency group) +* [`ruff`](https://github.com/astral-sh/ruff) (linting, part of optional `[dev]` dependency group) > [!NOTE] > We recommend using programs like [`uv`](https://github.com/astral-sh/uv) to manage your virtualenv (`venv`) to prevent @@ -82,7 +82,7 @@ uv run ruff format --check uv run pytest -v binds/python/test_unit.py # Run benchmarks -uv run python binds/python/benchmark.py +uv run pytest -v binds/python/test_bench.py --benchmark-only ``` ## License diff --git a/binds/python/benchmark.py b/binds/python/benchmark.py deleted file mode 100755 index 52647fc57..000000000 --- a/binds/python/benchmark.py +++ /dev/null @@ -1,72 +0,0 @@ -import secrets -import sys -import time - -from dashbls import ( - AugSchemeMPL, - G1Element, - G2Element, - PrivateKey, -) - - -def startStopwatch() -> float: - return time.perf_counter() - - -def endStopwatch(test_name: str, start: float, numIters: int) -> None: - end_time = time.perf_counter() - - duration = end_time - start - - print( - f"\n{test_name}\nTotal: {numIters} runs in {duration * 1000:.1f} ms\n" - f"Avg: {duration * 1000 / numIters:f}" - ) - - -def batch_verification() -> None: - - numIters = 100000 - sig_bytes = [] - pk_bytes = [] - ms = [] - - for i in range(numIters): - message = b"%d" % i - sk: PrivateKey = AugSchemeMPL.key_gen(secrets.token_bytes(32)) - pk: G1Element = sk.get_g1() - sig: G2Element = AugSchemeMPL.sign(sk, message) - - sig_bytes.append(bytes(sig)) - pk_bytes.append(bytes(pk)) - ms.append(message) - - pks = [] - - start = startStopwatch() - for pk in pk_bytes: - pks.append(G1Element.from_bytes(pk)) - - endStopwatch("Public key validation", start, numIters) - - sigs = [] - - start = startStopwatch() - for sig in sig_bytes: - sigs.append(G2Element.from_bytes(sig)) - endStopwatch("Signature validation", start, numIters) - - start = startStopwatch() - aggSig = AugSchemeMPL.aggregate(sigs) - endStopwatch("Aggregation", start, numIters) - - start = startStopwatch() - ok = AugSchemeMPL.aggregate_verify(pks, ms, aggSig) - endStopwatch("Batch verification", start, numIters) - if not ok: - print("aggregate_verification failed!") - sys.exit(1) - - -batch_verification() diff --git a/binds/python/test_bench.py b/binds/python/test_bench.py new file mode 100755 index 000000000..689d33ab0 --- /dev/null +++ b/binds/python/test_bench.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Benchmarks for the dashbls package.""" + +import secrets + +import pytest +from dashbls import ( + AugSchemeMPL, + G1Element, + G2Element, + PrivateKey, +) +from pytest_benchmark.fixture import BenchmarkFixture + +NUM_ITEMS = 1000 +SignedMessages = tuple[list[bytes], list[bytes], list[bytes]] + + +@pytest.fixture(scope="module") +def signed_messages() -> SignedMessages: + pk_bytes = [] + sig_bytes = [] + messages = [] + for i in range(NUM_ITEMS): + message = b"%d" % i + sk: PrivateKey = AugSchemeMPL.key_gen(secrets.token_bytes(32)) + pk: G1Element = sk.get_g1() + sig: G2Element = AugSchemeMPL.sign(sk, message) + + pk_bytes.append(bytes(pk)) + sig_bytes.append(bytes(sig)) + messages.append(message) + return pk_bytes, sig_bytes, messages + + +def test_public_key_validation( + benchmark: BenchmarkFixture, signed_messages: SignedMessages +) -> None: + pk_bytes, _, _ = signed_messages + pks = benchmark(lambda: [G1Element.from_bytes(pk) for pk in pk_bytes]) + assert len(pks) == NUM_ITEMS + + +def test_signature_validation(benchmark: BenchmarkFixture, signed_messages: SignedMessages) -> None: + _, sig_bytes, _ = signed_messages + sigs = benchmark(lambda: [G2Element.from_bytes(sig) for sig in sig_bytes]) + assert len(sigs) == NUM_ITEMS + + +def test_signature_aggregation( + benchmark: BenchmarkFixture, signed_messages: SignedMessages +) -> None: + _, sig_bytes, _ = signed_messages + sigs = [G2Element.from_bytes(sig) for sig in sig_bytes] + agg_sig = benchmark(lambda: AugSchemeMPL.aggregate(sigs)) + assert agg_sig is not None + + +def test_batch_verification(benchmark: BenchmarkFixture, signed_messages: SignedMessages) -> None: + pk_bytes, sig_bytes, messages = signed_messages + pks = [G1Element.from_bytes(pk) for pk in pk_bytes] + sigs = [G2Element.from_bytes(sig) for sig in sig_bytes] + agg_sig = AugSchemeMPL.aggregate(sigs) + + ok = benchmark(lambda: AugSchemeMPL.aggregate_verify(pks, messages, agg_sig)) + assert ok diff --git a/pyproject.toml b/pyproject.toml index 1fb350fb0..6587c7a3c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ Issues = "https://github.com/dashpay/bls-signatures/issues" dev = [ "ruff>=0.9", "pytest>=8.1", + "pytest-benchmark>=5.1", ] [build-system] @@ -27,6 +28,10 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] local_scheme = "no-local-version" +[tool.pytest.ini_options] +testpaths = ["binds/python"] +addopts = "--benchmark-skip" + [tool.ruff] indent-width = 4 line-length = 100 From 2d429ac683bc91bab586700f099ea28e1b06604f Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 17:46:46 +0530 Subject: [PATCH 20/31] refactor: make samples into distinct files, drop unneeded README --- README.md | 3 +- binds/python/README.md | 149 ------------------ binds/python/conftest.py | 15 ++ binds/python/samples/aggregate_signatures.py | 44 ++++++ .../samples/creating_keys_and_signatures.py | 36 +++++ binds/python/samples/hd_keys.py | 42 +++++ binds/python/samples/loading_from_bytes.py | 41 +++++ binds/python/samples/proof_of_possession.py | 58 +++++++ binds/python/samples/serializing_to_bytes.py | 36 +++++ binds/python/samples/tree_aggregates.py | 51 ++++++ binds/python/test_unit.py | 100 ++---------- pyproject.toml | 1 + 12 files changed, 340 insertions(+), 236 deletions(-) delete mode 100644 binds/python/README.md create mode 100644 binds/python/conftest.py create mode 100755 binds/python/samples/aggregate_signatures.py create mode 100755 binds/python/samples/creating_keys_and_signatures.py create mode 100755 binds/python/samples/hd_keys.py create mode 100755 binds/python/samples/loading_from_bytes.py create mode 100755 binds/python/samples/proof_of_possession.py create mode 100755 binds/python/samples/serializing_to_bytes.py create mode 100755 binds/python/samples/tree_aggregates.py diff --git a/README.md b/README.md index dbb2b499b..17f2ce073 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ cmake --build . --parallel 4 ## Build Python binds -Our Python binds target Python 3.10 or higher; they depend on +Our Python binds target Python 3.10 or higher; they depend on the following packages. Sample code is available +at [`binds/python/samples`](binds/python/samples). * [`pybind11`](https://github.com/pybind/pybind11) (bridging C++ and Python) * [`pytest`](https://github.com/pytest-dev/pytest) (benchmarks and unit tests, part of optional `[dev]` dependency group) diff --git a/binds/python/README.md b/binds/python/README.md deleted file mode 100644 index 7af234005..000000000 --- a/binds/python/README.md +++ /dev/null @@ -1,149 +0,0 @@ -# Python bindings - -Use the full power and efficiency of the C++ bls library, but in a few lines of python! - -## Install - -```bash -pip3 install dashbls - -``` - -Alternatively, to install from source, run the following, in the project root directory: - -```bash -pip3 install . -``` - -Cmake, a c++ compiler, and a recent version of pip3 (v18) are required for source install. -GMP(speed) is an optional dependency. -Public keys are G1Elements, and signatures are G2Elements. - -Then, to use: - -## Import the library - -```python -from dashbls import (PrivateKey, Util, AugSchemeMPL, PopSchemeMPL, - G1Element, G2Element) -``` - -## Creating keys and signatures - -```python -# Example seed, used to generate private key. Always use -# a secure RNG with sufficient entropy to generate a seed (at least 32 bytes). -seed: bytes = bytes([0, 50, 6, 244, 24, 199, 1, 25, 52, 88, 192, - 19, 18, 12, 89, 6, 220, 18, 102, 58, 209, 82, - 12, 62, 89, 110, 182, 9, 44, 20, 254, 22]) -sk: PrivateKey = AugSchemeMPL.key_gen(seed) -pk: G1Element = sk.get_g1() - -message: bytes = bytes([1, 2, 3, 4, 5]) -signature: G2Element = AugSchemeMPL.sign(sk, message) - -# Verify the signature -ok: bool = AugSchemeMPL.verify(pk, message, signature) -assert ok -``` - -## Serializing keys and signatures to bytes - -```python -sk_bytes: bytes = bytes(sk) # 32 bytes -pk_bytes: bytes = bytes(pk) # 48 bytes -signature_bytes: bytes = bytes(signature) # 96 bytes - -print(sk_bytes.hex(), pk_bytes.hex(), signature_bytes.hex()) -``` - -## Loading keys and signatures from bytes - -```python -sk = PrivateKey.from_bytes(sk_bytes) -pk = G1Element.from_bytes(pk_bytes) -signature = G2Element.from_bytes(signature_bytes) -``` - -## Create aggregate signatures - -```python -# Generate some more private keys -seed = bytes([1]) + seed[1:] -sk1: PrivateKey = AugSchemeMPL.key_gen(seed) -seed = bytes([2]) + seed[1:] -sk2: PrivateKey = AugSchemeMPL.key_gen(seed) -message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) - -# Generate first sig -pk1: G1Element = sk1.get_g1() -sig1: G2Element = AugSchemeMPL.sign(sk1, message) - -# Generate second sig -pk2: G1Element = sk2.get_g1() -sig2: G2Element = AugSchemeMPL.sign(sk2, message2) - -# Signatures can be non-interactively combined by anyone -agg_sig: G2Element = AugSchemeMPL.aggregate([sig1, sig2]) - -ok = AugSchemeMPL.aggregate_verify([pk1, pk2], [message, message2], agg_sig) -``` - -## Arbitrary trees of aggregates - -```python -seed = bytes([3]) + seed[1:] -sk3: PrivateKey = AugSchemeMPL.key_gen(seed) -pk3: G1Element = sk3.get_g1() -message3: bytes = bytes([100, 2, 254, 88, 90, 45, 23]) -sig3: G2Element = AugSchemeMPL.sign(sk3, message3) - -agg_sig_final: G2Element = AugSchemeMPL.aggregate([agg_sig, sig3]) -ok = AugSchemeMPL.aggregate_verify([pk1, pk2, pk3], [message, message2, message3], agg_sig_final) -``` - -## Very fast verification with Proof of Possession scheme - -```python -# If the same message is signed, you can use Proof of Posession (PopScheme) for efficiency -# A proof of possession MUST be passed around with the PK to ensure security. -pop_sig1: G2Element = PopSchemeMPL.sign(sk1, message) -pop_sig2: G2Element = PopSchemeMPL.sign(sk2, message) -pop_sig3: G2Element = PopSchemeMPL.sign(sk3, message) -pop1: G2Element = PopSchemeMPL.pop_prove(sk1) -pop2: G2Element = PopSchemeMPL.pop_prove(sk2) -pop3: G2Element = PopSchemeMPL.pop_prove(sk3) - -ok = PopSchemeMPL.pop_verify(pk1, pop1) -ok = PopSchemeMPL.pop_verify(pk2, pop2) -ok = PopSchemeMPL.pop_verify(pk3, pop3) - -pop_sig_agg: G2Element = PopSchemeMPL.aggregate([pop_sig1, pop_sig2, pop_sig3]) - -ok = PopSchemeMPL.fast_aggregate_verify([pk1, pk2, pk3], message, pop_sig_agg) - -# Aggregate public key, indistinguishable from a single public key -pop_agg_pk: G1Element = pk1 + pk2 + pk3 -ok = PopSchemeMPL.verify(pop_agg_pk, message, pop_sig_agg) - -# Aggregate private keys -pop_agg_sk: PrivateKey = PrivateKey.aggregate([sk1, sk2, sk3]) -ok = PopSchemeMPL.sign(pop_agg_sk, message) == pop_sig_agg -``` - -## HD keys using [EIP-2333](https://github.com/ethereum/EIPs/pull/2333) - -```python -master_sk: PrivateKey = AugSchemeMPL.key_gen(seed) -child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) -grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) - -master_pk: G1Element = master_sk.get_g1() -child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) -grandchild_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(child_u, 0) - -child_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(master_pk, 22) -grandchild_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(child_u_pk, 0) - -ok = (grandchild_u_pk == grandchild_u.get_g1()) -``` diff --git a/binds/python/conftest.py b/binds/python/conftest.py new file mode 100644 index 000000000..d595d6ea8 --- /dev/null +++ b/binds/python/conftest.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Allow samples to be imported by name.""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent / "samples")) diff --git a/binds/python/samples/aggregate_signatures.py b/binds/python/samples/aggregate_signatures.py new file mode 100755 index 000000000..014d1fac0 --- /dev/null +++ b/binds/python/samples/aggregate_signatures.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Aggregate signatures from multiple keys over multiple messages.""" + +from dashbls import AugSchemeMPL, G1Element, G2Element, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def aggregate_signatures() -> None: + # Two independent signers, each with their own message + sk1: PrivateKey = AugSchemeMPL.key_gen(bytes([1]) + SEED[1:]) + sk2: PrivateKey = AugSchemeMPL.key_gen(bytes([2]) + SEED[1:]) + + message1: bytes = bytes([1, 2, 3, 4, 5]) + message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) + + pk1: G1Element = sk1.get_g1() + sig1: G2Element = AugSchemeMPL.sign(sk1, message1) + + pk2: G1Element = sk2.get_g1() + sig2: G2Element = AugSchemeMPL.sign(sk2, message2) + + # Signatures can be non-interactively combined by anyone + agg_sig: G2Element = AugSchemeMPL.aggregate([sig1, sig2]) + + # Verifying an aggregate over distinct messages needs every public key and + # every message, in the order they were signed in + ok = AugSchemeMPL.aggregate_verify([pk1, pk2], [message1, message2], agg_sig) + assert ok + + print(f"aggregate signature: {bytes(agg_sig).hex()}") + + +if __name__ == "__main__": + aggregate_signatures() diff --git a/binds/python/samples/creating_keys_and_signatures.py b/binds/python/samples/creating_keys_and_signatures.py new file mode 100755 index 000000000..8fac6c0a2 --- /dev/null +++ b/binds/python/samples/creating_keys_and_signatures.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Generate a keypair and produce/verify a signature.""" + +from dashbls import AugSchemeMPL, G1Element, G2Element, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def creating_keys_and_signatures() -> None: + # Example seed. Always use a secure RNG with sufficient entropy to generate + # a seed (at least 32 bytes). + sk: PrivateKey = AugSchemeMPL.key_gen(SEED) + pk: G1Element = sk.get_g1() + + message: bytes = bytes([1, 2, 3, 4, 5]) + signature: G2Element = AugSchemeMPL.sign(sk, message) + + # Verify the signature + ok: bool = AugSchemeMPL.verify(pk, message, signature) + assert ok + + print(f"public key: {bytes(pk).hex()}") + print(f"signature: {bytes(signature).hex()}") + + +if __name__ == "__main__": + creating_keys_and_signatures() diff --git a/binds/python/samples/hd_keys.py b/binds/python/samples/hd_keys.py new file mode 100755 index 000000000..2e34c00da --- /dev/null +++ b/binds/python/samples/hd_keys.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Derive hardened and unhardened HD keys, per EIP-2333.""" + +from dashbls import AugSchemeMPL, G1Element, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def hd_keys() -> None: + master_sk: PrivateKey = AugSchemeMPL.key_gen(SEED) + master_pk: G1Element = master_sk.get_g1() + + # Hardened derivation needs the private key, and the resulting child cannot + # be derived from the public key alone + child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) + grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) + + # Unhardened derivation can be mirrored on the public side, so a watch-only + # holder of the master public key can compute the same child public keys + child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) + grandchild_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(child_u, 0) + + child_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(master_pk, 22) + grandchild_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(child_u_pk, 0) + + assert grandchild_u_pk == grandchild_u.get_g1() + + print(f"hardened grandchild: {bytes(grandchild.get_g1()).hex()}") + print(f"unhardened grandchild: {bytes(grandchild_u_pk).hex()}") + + +if __name__ == "__main__": + hd_keys() diff --git a/binds/python/samples/loading_from_bytes.py b/binds/python/samples/loading_from_bytes.py new file mode 100755 index 000000000..401e1305e --- /dev/null +++ b/binds/python/samples/loading_from_bytes.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Deserialize keys and signatures from bytes.""" + +from dashbls import AugSchemeMPL, G1Element, G2Element, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def loading_from_bytes() -> None: + # Stand in for bytes that arrived over the wire or came off disk + original_sk: PrivateKey = AugSchemeMPL.key_gen(SEED) + message: bytes = bytes([1, 2, 3, 4, 5]) + sk_bytes: bytes = bytes(original_sk) + pk_bytes: bytes = bytes(original_sk.get_g1()) + signature_bytes: bytes = bytes(AugSchemeMPL.sign(original_sk, message)) + + # from_bytes validates: it rejects points that are malformed or outside the + # correct subgroup, and raises ValueError rather than returning junk + sk: PrivateKey = PrivateKey.from_bytes(sk_bytes) + pk: G1Element = G1Element.from_bytes(pk_bytes) + signature: G2Element = G2Element.from_bytes(signature_bytes) + + assert sk == original_sk + assert pk == sk.get_g1() + assert AugSchemeMPL.verify(pk, message, signature) + + print(f"public key: {bytes(pk).hex()}") + print(f"signature: {bytes(signature).hex()}") + + +if __name__ == "__main__": + loading_from_bytes() diff --git a/binds/python/samples/proof_of_possession.py b/binds/python/samples/proof_of_possession.py new file mode 100755 index 000000000..e327dcfc9 --- /dev/null +++ b/binds/python/samples/proof_of_possession.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Verify aggregated signatures quickly using the Proof of Possession scheme.""" + +from dashbls import G1Element, PopSchemeMPL, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def proof_of_possession() -> None: + sk1: PrivateKey = PopSchemeMPL.key_gen(bytes([1]) + SEED[1:]) + sk2: PrivateKey = PopSchemeMPL.key_gen(bytes([2]) + SEED[1:]) + sk3: PrivateKey = PopSchemeMPL.key_gen(bytes([3]) + SEED[1:]) + + pk1: G1Element = sk1.get_g1() + pk2: G1Element = sk2.get_g1() + pk3: G1Element = sk3.get_g1() + + message: bytes = bytes([1, 2, 3, 4, 5]) + + # If the same message is signed, you can use Proof of Possession (PopScheme) for efficiency. + # A proof of possession MUST be passed around with the PK to ensure security. + pop_sig1 = PopSchemeMPL.sign(sk1, message) + pop_sig2 = PopSchemeMPL.sign(sk2, message) + pop_sig3 = PopSchemeMPL.sign(sk3, message) + pop1 = PopSchemeMPL.pop_prove(sk1) + pop2 = PopSchemeMPL.pop_prove(sk2) + pop3 = PopSchemeMPL.pop_prove(sk3) + + assert PopSchemeMPL.pop_verify(pk1, pop1) + assert PopSchemeMPL.pop_verify(pk2, pop2) + assert PopSchemeMPL.pop_verify(pk3, pop3) + + pop_sig_agg = PopSchemeMPL.aggregate([pop_sig1, pop_sig2, pop_sig3]) + + assert PopSchemeMPL.fast_aggregate_verify([pk1, pk2, pk3], message, pop_sig_agg) + + # Aggregate public key, indistinguishable from a single public key + pop_agg_pk: G1Element = pk1 + pk2 + pk3 + assert PopSchemeMPL.verify(pop_agg_pk, message, pop_sig_agg) + + # Aggregate private keys + pop_agg_sk: PrivateKey = PrivateKey.aggregate([sk1, sk2, sk3]) + assert PopSchemeMPL.sign(pop_agg_sk, message) == pop_sig_agg + + print(f"aggregate public key: {bytes(pop_agg_pk).hex()}") + + +if __name__ == "__main__": + proof_of_possession() diff --git a/binds/python/samples/serializing_to_bytes.py b/binds/python/samples/serializing_to_bytes.py new file mode 100755 index 000000000..5cb887d58 --- /dev/null +++ b/binds/python/samples/serializing_to_bytes.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Serialize keys and signatures to bytes.""" + +from dashbls import AugSchemeMPL, G1Element, G2Element, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def serializing_to_bytes() -> None: + sk: PrivateKey = AugSchemeMPL.key_gen(SEED) + pk: G1Element = sk.get_g1() + signature: G2Element = AugSchemeMPL.sign(sk, bytes([1, 2, 3, 4, 5])) + + sk_bytes: bytes = bytes(sk) # 32 bytes + pk_bytes: bytes = bytes(pk) # 48 bytes + signature_bytes: bytes = bytes(signature) # 96 bytes + + assert (len(sk_bytes), len(pk_bytes), len(signature_bytes)) == (32, 48, 96) + + # Serialized private keys are secret material; treat them the way you would + # treat the seed they came from, and never log them. + print(f"public key: {pk_bytes.hex()}") + print(f"signature: {signature_bytes.hex()}") + + +if __name__ == "__main__": + serializing_to_bytes() diff --git a/binds/python/samples/tree_aggregates.py b/binds/python/samples/tree_aggregates.py new file mode 100755 index 000000000..6260782d5 --- /dev/null +++ b/binds/python/samples/tree_aggregates.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2020-present, Chia Network Inc. +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: Apache-2.0 +# See the accompanying file LICENSE or https://opensource.org/licenses/Apache-2.0 +# + +"""Build an aggregate signature out of an aggregate signature and another signature.""" + +from dashbls import AugSchemeMPL, G1Element, G2Element, PrivateKey + +SEED: bytes = bytes.fromhex("003206f418c701193458c013120c5906dc12663ad1520c3e596eb6092c14fe16") + + +def tree_aggregates() -> None: + sk1: PrivateKey = AugSchemeMPL.key_gen(bytes([1]) + SEED[1:]) + sk2: PrivateKey = AugSchemeMPL.key_gen(bytes([2]) + SEED[1:]) + sk3: PrivateKey = AugSchemeMPL.key_gen(bytes([3]) + SEED[1:]) + + message1: bytes = bytes([1, 2, 3, 4, 5]) + message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) + message3: bytes = bytes([100, 2, 254, 88, 90, 45, 23]) + + pk1: G1Element = sk1.get_g1() + pk2: G1Element = sk2.get_g1() + pk3: G1Element = sk3.get_g1() + + # Aggregate the first two signatures on their own... + agg_sig: G2Element = AugSchemeMPL.aggregate( + [AugSchemeMPL.sign(sk1, message1), AugSchemeMPL.sign(sk2, message2)] + ) + + # ...then fold a third signature into that aggregate. Aggregation composes, + # so an aggregate is indistinguishable from a plain signature to the next + # round, and the tree can be built in any shape. + sig3: G2Element = AugSchemeMPL.sign(sk3, message3) + agg_sig_final: G2Element = AugSchemeMPL.aggregate([agg_sig, sig3]) + + ok = AugSchemeMPL.aggregate_verify( + [pk1, pk2, pk3], [message1, message2, message3], agg_sig_final + ) + assert ok + + print(f"tree aggregate: {bytes(agg_sig_final).hex()}") + + +if __name__ == "__main__": + tree_aggregates() diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py index ff0bcbb21..726707aa1 100755 --- a/binds/python/test_unit.py +++ b/binds/python/test_unit.py @@ -11,6 +11,7 @@ """Unit tests for the dashbls package.""" import binascii +import importlib import pytest from dashbls import ( @@ -243,94 +244,21 @@ def test_sign_matches_reference_vectors(scheme: type) -> None: assert bytes(sig_agg) == ref_sig_agg -def test_readme() -> None: - seed: bytes = SEED - sk: PrivateKey = AugSchemeMPL.key_gen(seed) - pk: G1Element = sk.get_g1() - - message: bytes = bytes([1, 2, 3, 4, 5]) - signature: G2Element = AugSchemeMPL.sign(sk, message) - - ok: bool = AugSchemeMPL.verify(pk, message, signature) - assert ok - - sk_bytes: bytes = bytes(sk) # 32 bytes - pk_bytes: bytes = bytes(pk) # 48 bytes - signature_bytes: bytes = bytes(signature) # 96 bytes - - sk = PrivateKey.from_bytes(sk_bytes) - pk = G1Element.from_bytes(pk_bytes) - signature = G2Element.from_bytes(signature_bytes) - - seed = bytes([1]) + seed[1:] - sk1: PrivateKey = AugSchemeMPL.key_gen(seed) - seed = bytes([2]) + seed[1:] - sk2: PrivateKey = AugSchemeMPL.key_gen(seed) - message2: bytes = bytes([1, 2, 3, 4, 5, 6, 7]) - - pk1: G1Element = sk1.get_g1() - sig1: G2Element = AugSchemeMPL.sign(sk1, message) - - pk2: G1Element = sk2.get_g1() - sig2: G2Element = AugSchemeMPL.sign(sk2, message2) - - agg_sig: G2Element = AugSchemeMPL.aggregate([sig1, sig2]) - - ok = AugSchemeMPL.aggregate_verify([pk1, pk2], [message, message2], agg_sig) - assert ok - - seed = bytes([3]) + seed[1:] - sk3: PrivateKey = AugSchemeMPL.key_gen(seed) - pk3: G1Element = sk3.get_g1() - message3: bytes = bytes([100, 2, 254, 88, 90, 45, 23]) - sig3: G2Element = AugSchemeMPL.sign(sk3, message3) - - agg_sig_final: G2Element = AugSchemeMPL.aggregate([agg_sig, sig3]) - ok = AugSchemeMPL.aggregate_verify( - [pk1, pk2, pk3], [message, message2, message3], agg_sig_final - ) - assert ok - - pop_sig1: G2Element = PopSchemeMPL.sign(sk1, message) - pop_sig2: G2Element = PopSchemeMPL.sign(sk2, message) - pop_sig3: G2Element = PopSchemeMPL.sign(sk3, message) - pop1: G2Element = PopSchemeMPL.pop_prove(sk1) - pop2: G2Element = PopSchemeMPL.pop_prove(sk2) - pop3: G2Element = PopSchemeMPL.pop_prove(sk3) - - ok = PopSchemeMPL.pop_verify(pk1, pop1) - assert ok - ok = PopSchemeMPL.pop_verify(pk2, pop2) - assert ok - ok = PopSchemeMPL.pop_verify(pk3, pop3) - assert ok - - pop_sig_agg: G2Element = PopSchemeMPL.aggregate([pop_sig1, pop_sig2, pop_sig3]) - - ok = PopSchemeMPL.fast_aggregate_verify([pk1, pk2, pk3], message, pop_sig_agg) - assert ok - - pop_agg_pk: G1Element = pk1 + pk2 + pk3 - ok = PopSchemeMPL.verify(pop_agg_pk, message, pop_sig_agg) - assert ok - - pop_agg_sk: PrivateKey = PrivateKey.aggregate([sk1, sk2, sk3]) - ok = PopSchemeMPL.sign(pop_agg_sk, message) == pop_sig_agg - assert ok - - master_sk: PrivateKey = AugSchemeMPL.key_gen(seed) - child: PrivateKey = AugSchemeMPL.derive_child_sk(master_sk, 152) - _grandchild: PrivateKey = AugSchemeMPL.derive_child_sk(child, 952) - - master_pk: G1Element = master_sk.get_g1() - child_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(master_sk, 22) - grandchild_u: PrivateKey = AugSchemeMPL.derive_child_sk_unhardened(child_u, 0) +SAMPLES = [ + "aggregate_signatures", + "creating_keys_and_signatures", + "hd_keys", + "loading_from_bytes", + "proof_of_possession", + "serializing_to_bytes", + "tree_aggregates", +] - child_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(master_pk, 22) - grandchild_u_pk: G1Element = AugSchemeMPL.derive_child_pk_unhardened(child_u_pk, 0) - ok = grandchild_u_pk == grandchild_u.get_g1() - assert ok +@pytest.mark.parametrize("sample", SAMPLES) +def test_samples(sample: str) -> None: + module = importlib.import_module(sample) + getattr(module, sample)() def test_aggregate_verify_zero_items() -> None: diff --git a/pyproject.toml b/pyproject.toml index 6587c7a3c..6dbfb3fb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,7 @@ select = [ [tool.ruff.lint.per-file-ignores] "binds/python/test*.py" = ["S101"] +"binds/python/samples/*.py" = ["S101"] [tool.ruff.format] indent-style = "space" From 4d7934d8d5df2802b9ccfecdeb5f51b2dcd13157 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 02:46:18 +0530 Subject: [PATCH 21/31] fix: bind G1Element/G2Element scalar multiplication against PrivateKey --- binds/python/pythonbindings.cpp | 16 ++++++++-------- binds/python/test_unit.py | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index e3df04d58..841375202 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -449,16 +449,16 @@ PYBIND11_MODULE(dashbls, m) py::is_operator()) .def( "__mul__", - [](G1Element &self, bn_t other) { + [](G1Element &self, const PrivateKey &other) { py::gil_scoped_release release; - return self * (*(bn_t *)&other); + return self * other; }, py::is_operator()) .def( "__rmul__", - [](G1Element &self, bn_t other) { + [](G1Element &self, const PrivateKey &other) { py::gil_scoped_release release; - return self * (*(bn_t *)&other); + return other * self; }, py::is_operator()) .def( @@ -590,16 +590,16 @@ PYBIND11_MODULE(dashbls, m) py::is_operator()) .def( "__mul__", - [](G2Element &self, bn_t other) { + [](G2Element &self, const PrivateKey &other) { py::gil_scoped_release release; - return self * (*(bn_t *)&other); + return self * other; }, py::is_operator()) .def( "__rmul__", - [](G2Element &self, bn_t other) { + [](G2Element &self, const PrivateKey &other) { py::gil_scoped_release release; - return self * (*(bn_t *)&other); + return other * self; }, py::is_operator()) diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py index 726707aa1..6bdba1041 100755 --- a/binds/python/test_unit.py +++ b/binds/python/test_unit.py @@ -56,6 +56,27 @@ def test_private_key_and_public_key_roundtrip() -> None: assert pk == G1Element.from_bytes(bytes(pk)) +def test_scalar_multiplication_by_private_key() -> None: + """The operators took `bn_t`, which pybind11 has no caster for, so every call + raised TypeError regardless of argument.""" + sk = BasicSchemeMPL.key_gen(SEED) + g1 = G1Element.generator() + assert g1 * sk == sk.get_g1() + assert sk * g1 == sk.get_g1() + + g2 = G2Element.generator() + assert g2 * sk == sk * g2 + assert g2 * sk != G2Element() + + +def test_scalar_multiplication_is_additive( + keypairs: tuple[PrivateKey, G1Element, PrivateKey, G1Element], +) -> None: + sk1, _pk1, sk2, _pk2 = keypairs + g1 = G1Element.generator() + assert (g1 * sk1) + (g1 * sk2) == g1 * PrivateKey.aggregate([sk1, sk2]) + + @pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) def test_sign_and_verify_roundtrip(scheme: type) -> None: sk = BasicSchemeMPL.key_gen(SEED) From fcd4ebb437a73ea540f2b40a716a2c038ea80136 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 02:46:39 +0530 Subject: [PATCH 22/31] fix: accept from_message's domain separation tag as bytes --- binds/python/pythonbindings.cpp | 28 ++++++++++++++++++++++++++-- binds/python/test_unit.py | 18 ++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index 841375202..c86448ccc 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -428,7 +428,19 @@ PYBIND11_MODULE(dashbls, m) return G1Element::FromBytesUnchecked({data_ptr, G1Element::SIZE}); }) .def("generator", &G1Element::Generator) - .def("from_message", py::overload_cast&, const uint8_t*, int>(&G1Element::FromMessage), py::call_guard()) + .def_static( + "from_message", + [](const py::bytes &msg, const py::bytes &dst) { + const auto msg_str = std::string(msg); + const auto dst_str = std::string(dst); + py::gil_scoped_release release; + return G1Element::FromMessage( + Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()), + (const uint8_t *)dst_str.c_str(), + (int)dst_str.size()); + }, + py::arg("msg"), + py::arg("dst")) .def("pair", &G1Element::Pair, py::call_guard()) .def("negate", &G1Element::Negate, py::call_guard()) .def("get_fingerprint", &G1Element::GetFingerprint, py::call_guard()) @@ -570,7 +582,19 @@ PYBIND11_MODULE(dashbls, m) return G2Element::FromBytesUnchecked({data_ptr, G2Element::SIZE}); }) .def("generator", &G2Element::Generator) - .def("from_message", py::overload_cast&, const uint8_t*, int, bool>(&G2Element::FromMessage), py::call_guard()) + .def_static( + "from_message", + [](const py::bytes &msg, const py::bytes &dst) { + const auto msg_str = std::string(msg); + const auto dst_str = std::string(dst); + py::gil_scoped_release release; + return G2Element::FromMessage( + Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()), + (const uint8_t *)dst_str.c_str(), + (int)dst_str.size()); + }, + py::arg("msg"), + py::arg("dst")) .def("pair", &G2Element::Pair, py::call_guard()) .def("negate", &G2Element::Negate, py::call_guard()) .def( diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py index 6bdba1041..f00567746 100755 --- a/binds/python/test_unit.py +++ b/binds/python/test_unit.py @@ -286,6 +286,24 @@ def test_aggregate_verify_zero_items() -> None: assert AugSchemeMPL.aggregate_verify([], [], G2Element()) +G1_DST = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" +G2_DST = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_" + + +def test_from_message() -> None: + msg = bytes([10]) * 32 + assert G2Element.from_message(msg, G2_DST) == BasicSchemeMPL.g2_from_message(msg) + assert G1Element.from_message(msg, G1_DST) != G1Element() + + +def test_from_message_is_domain_separated() -> None: + """The tag keeps one scheme's hashes off another's, so a different tag over + the same message must land elsewhere.""" + msg = bytes([10]) * 32 + assert G2Element.from_message(msg, G2_DST) != G2Element.from_message(msg, G1_DST) + assert G1Element.from_message(msg, G1_DST) != G1Element.from_message(msg, G2_DST) + + def test_from_bytes_and_from_bytes_unchecked_agree_on_valid_point() -> None: sk1 = BasicSchemeMPL.key_gen(b"1" * 32) good_point_bytes = bytes(sk1.get_g1()) From cb0d8ca85139d5e939e7836b43e977a5f422e7a4 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 12:37:13 +0530 Subject: [PATCH 23/31] fix: reject oversized from_message domain separation tags --- binds/python/pythonbindings.cpp | 22 ++++++++++++++++++++-- binds/python/test_unit.py | 13 +++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index c86448ccc..cb2264a14 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -16,6 +16,9 @@ #include #include +#include +#include + #include #include #include @@ -34,6 +37,21 @@ inline int PyLong_AsByteArray(PyLongObject* obj, uint8_t* buf, Py_ssize_t size, #endif // PY_VERSION_HEX >= 0x030d0000 ); } + +// md_xmd caps a tag at 255 bytes but compares signed, so a tag at or beyond 2 GiB +// truncates negative, slips the guard and is widened back to a huge length. +std::string CopyDst(const py::bytes &dst, const char *who) +{ + // Measure before copying: the tags this rejects are precisely the ones too + // large to want a second copy of. + const auto size = py::len(dst); + if (size > 255) { + throw std::invalid_argument( + std::string(who) + ": domain separation tag must be at most 255 bytes, got " + + std::to_string(size)); + } + return std::string(dst); +} } // anonymous namespace PYBIND11_MODULE(dashbls, m) @@ -432,7 +450,7 @@ PYBIND11_MODULE(dashbls, m) "from_message", [](const py::bytes &msg, const py::bytes &dst) { const auto msg_str = std::string(msg); - const auto dst_str = std::string(dst); + const auto dst_str = CopyDst(dst, "G1Element.from_message"); py::gil_scoped_release release; return G1Element::FromMessage( Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()), @@ -586,7 +604,7 @@ PYBIND11_MODULE(dashbls, m) "from_message", [](const py::bytes &msg, const py::bytes &dst) { const auto msg_str = std::string(msg); - const auto dst_str = std::string(dst); + const auto dst_str = CopyDst(dst, "G2Element.from_message"); py::gil_scoped_release release; return G2Element::FromMessage( Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()), diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py index f00567746..37ec16d01 100755 --- a/binds/python/test_unit.py +++ b/binds/python/test_unit.py @@ -304,6 +304,19 @@ def test_from_message_is_domain_separated() -> None: assert G1Element.from_message(msg, G1_DST) != G1Element.from_message(msg, G2_DST) +@pytest.mark.parametrize("element", [G1Element, G2Element]) +@pytest.mark.parametrize("size", [256, 1000]) +def test_from_message_rejects_oversized_dst(element: type, size: int) -> None: + """md_xmd caps the tag at 255 bytes but compares signed, so the length is + checked here rather than reaching relic.""" + with pytest.raises(ValueError): + element.from_message(bytes([10]) * 32, b"x" * size) + + +def test_from_message_accepts_maximum_dst() -> None: + assert G2Element.from_message(bytes([10]) * 32, b"x" * 255) != G2Element() + + def test_from_bytes_and_from_bytes_unchecked_agree_on_valid_point() -> None: sk1 = BasicSchemeMPL.key_gen(b"1" * 32) good_point_bytes = bytes(sk1.get_g1()) From fee06d914ebc01ec1b69282cfb19d4c6ab4ed91d Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:37:12 +0530 Subject: [PATCH 24/31] fix: reject non-contiguous buffers at every parsing entry point --- binds/python/pythonbindings.cpp | 161 +++++++++----------------------- binds/python/test_unit.py | 35 +++++++ 2 files changed, 78 insertions(+), 118 deletions(-) diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index cb2264a14..4aa8496d7 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -16,6 +16,10 @@ #include #include +#include +#include +#include +#include #include #include @@ -52,6 +56,33 @@ std::string CopyDst(const py::bytes &dst, const char *who) } return std::string(dst); } + +// Bytes is a pointer and a length, so a strided view has no counterpart to +// mirror; ask CPython for a contiguous one and let it raise, rather than +// reinterpreting info.ptr as contiguous and reading past the caller's view. +template +std::array CopyBuffer(const py::buffer &b, const char *what) +{ + auto *view = new Py_buffer(); + if (PyObject_GetBuffer(b.ptr(), view, PyBUF_C_CONTIGUOUS | PyBUF_FORMAT) != 0) { + delete view; + throw py::error_already_set(); + } + py::buffer_info info(view); + + if (info.format != py::format_descriptor::format() || info.ndim != 1) + throw std::runtime_error("Incompatible buffer format!"); + + if (info.size != static_cast(N)) { + throw std::invalid_argument( + std::string("Length of bytes object not equal to ") + what); + } + + std::array data; + const auto *data_ptr = reinterpret_cast(info.ptr); + std::copy(data_ptr, data_ptr + N, data.data()); + return data; +} } // anonymous namespace PYBIND11_MODULE(dashbls, m) @@ -63,18 +94,7 @@ PYBIND11_MODULE(dashbls, m) .def( "from_bytes", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != PrivateKey::PRIVATE_KEY_SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to PrivateKey::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "PrivateKey::SIZE"); py::gil_scoped_release release; return PrivateKey::FromBytes(data); }) @@ -397,53 +417,22 @@ PYBIND11_MODULE(dashbls, m) return G1Element::FromBytes(buffer); })) .def(py::init([](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != G1Element::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to G1Element::SIZE"); - } - auto data_ptr = static_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "G1Element::SIZE"); py::gil_scoped_release release; return G1Element::FromBytes(data); })) .def( "from_bytes", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != G1Element::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to G1Element::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "G1Element::SIZE"); py::gil_scoped_release release; return G1Element::FromBytes(data); }) .def( "from_bytes_unchecked", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != G1Element::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to G1Element::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - return G1Element::FromBytesUnchecked({data_ptr, G1Element::SIZE}); + auto data = CopyBuffer(b, "G1Element::SIZE"); + return G1Element::FromBytesUnchecked(data); }) .def("generator", &G1Element::Generator) .def_static( @@ -538,18 +527,7 @@ PYBIND11_MODULE(dashbls, m) })) .def(py::init(&G2Element::FromByteVector), py::call_guard()) .def(py::init([](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != G2Element::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to G2Element::SIZE"); - } - auto data_ptr = static_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "G2Element::SIZE"); py::gil_scoped_release release; return G2Element::FromBytes(data); })) @@ -569,35 +547,15 @@ PYBIND11_MODULE(dashbls, m) .def( "from_bytes", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != G2Element::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to G2Element::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "G2Element::SIZE"); py::gil_scoped_release release; return G2Element::FromBytes(data); }) .def( "from_bytes_unchecked", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != G2Element::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to G2Element::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - return G2Element::FromBytesUnchecked({data_ptr, G2Element::SIZE}); + auto data = CopyBuffer(b, "G2Element::SIZE"); + return G2Element::FromBytesUnchecked(data); }) .def("generator", &G2Element::Generator) .def_static( @@ -682,18 +640,7 @@ PYBIND11_MODULE(dashbls, m) "SIZE", [](py::object self) { return GTElement::SIZE; }) .def(py::init(>Element::FromByteVector), py::call_guard()) .def(py::init([](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != GTElement::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to GTElement::SIZE"); - } - auto data_ptr = static_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "GTElement::SIZE"); py::gil_scoped_release release; return GTElement::FromBytes(data); })) @@ -713,36 +660,14 @@ PYBIND11_MODULE(dashbls, m) .def( "from_bytes", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != GTElement::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to GTElement::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "GTElement::SIZE"); py::gil_scoped_release release; return GTElement::FromBytes(data); }) .def( "from_bytes_unchecked", [](py::buffer const b) { - py::buffer_info info = b.request(); - if (info.format != py::format_descriptor::format() || - info.ndim != 1) - throw std::runtime_error("Incompatible buffer format!"); - - if ((int)info.size != GTElement::SIZE) { - throw std::invalid_argument( - "Length of bytes object not equal to GTElement::SIZE"); - } - auto data_ptr = reinterpret_cast(info.ptr); - std::array data; - std::copy(data_ptr, data_ptr + data.size(), data.data()); + auto data = CopyBuffer(b, "GTElement::SIZE"); py::gil_scoped_release release; return GTElement::FromBytesUnchecked(data); }) diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py index 37ec16d01..c4733a5c6 100755 --- a/binds/python/test_unit.py +++ b/binds/python/test_unit.py @@ -19,6 +19,7 @@ BasicSchemeMPL, G1Element, G2Element, + GTElement, PopSchemeMPL, PrivateKey, ) @@ -337,3 +338,37 @@ def test_from_bytes_rejects_invalid_g2_point() -> None: bad_g2_point_hex = "8f2886c94eaeac335c8414cbf14c16681b225380cfee3293becc4531d5b415984b4ea4050d9ecda11fbc21c60627e9d212dfcb17d2b5ae399aa3fbcb099e05baa496b852ad976fb633cc6766b02fca4da549dc063908463b2906ad64e8b310ad" # noqa: E501 with pytest.raises(ValueError): G2Element.from_bytes(bytes.fromhex(bad_g2_point_hex)) + + +@pytest.mark.parametrize( + ("cls", "size"), + [ + (PrivateKey, PrivateKey.PRIVATE_KEY_SIZE), + (G1Element, G1Element.SIZE), + (G2Element, G2Element.SIZE), + (GTElement, GTElement.SIZE), + ], + ids=["PrivateKey", "G1Element", "G2Element", "GTElement"], +) +def test_from_bytes_rejects_non_contiguous_buffers(cls: type, size: int) -> None: + # A reversed view points at the last backing byte with a stride of -1, so + # reading it as if it were contiguous runs off the end of the allocation + with pytest.raises(BufferError): + cls.from_bytes(memoryview(bytearray(size))[::-1]) + # A strided view stays in bounds but is not the bytes the caller passed + with pytest.raises(BufferError): + cls.from_bytes(memoryview(bytearray(size * 2))[::2]) + + +@pytest.mark.parametrize( + ("cls", "size"), + [ + (G1Element, G1Element.SIZE), + (G2Element, G2Element.SIZE), + (GTElement, GTElement.SIZE), + ], + ids=["G1Element", "G2Element", "GTElement"], +) +def test_from_bytes_unchecked_rejects_non_contiguous_buffers(cls: type, size: int) -> None: + with pytest.raises(BufferError): + cls.from_bytes_unchecked(memoryview(bytearray(size))[::-1]) From 60e015a8cd2d201be487072ceb992feeda6dea41 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 3 Aug 2026 17:54:40 +0530 Subject: [PATCH 25/31] fix: serialize relic access behind a process-wide lock --- binds/python/pythonbindings.cpp | 205 ++++++++++++++++++-------------- 1 file changed, 116 insertions(+), 89 deletions(-) diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index 4aa8496d7..76c3be58c 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -18,6 +18,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,22 @@ inline int PyLong_AsByteArray(PyLongObject* obj, uint8_t* buf, Py_ssize_t size, ); } +// relic's context is process-wide here (MULTI is unset, see setup.py), so a +// released GIL leaves threads racing on its error code and PRNG state. The +// GIL goes first so a waiter cannot strand the holder that must retake it. +std::mutex &RelicMutex() +{ + static std::mutex mutex; + return mutex; +} + +struct RelicGuard { + RelicGuard() : lock(RelicMutex()) {} + + py::gil_scoped_release release; + std::lock_guard lock; +}; + // md_xmd caps a tag at 255 bytes but compares signed, so a tag at or beyond 2 GiB // truncates negative, slips the guard and is widened back to a huge length. std::string CopyDst(const py::bytes &dst, const char *who) @@ -95,7 +112,7 @@ PYBIND11_MODULE(dashbls, m) "from_bytes", [](py::buffer const b) { auto data = CopyBuffer(b, "PrivateKey::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return PrivateKey::FromBytes(data); }) .def( @@ -104,7 +121,7 @@ PYBIND11_MODULE(dashbls, m) uint8_t *output = Util::SecAlloc(PrivateKey::PRIVATE_KEY_SIZE); { - py::gil_scoped_release release; + RelicGuard guard; k.Serialize(output); } py::bytes ret = py::bytes( @@ -116,17 +133,18 @@ PYBIND11_MODULE(dashbls, m) .def( "__deepcopy__", [](const PrivateKey &k, const py::object &memo) { + RelicGuard guard; return PrivateKey(k); }) .def("get_g1", [](const PrivateKey &k) { - py::gil_scoped_release release; + RelicGuard guard; return k.GetG1Element(); }) - .def("aggregate", &PrivateKey::Aggregate, py::call_guard()) - .def(py::self == py::self) - .def(py::self != py::self) + .def("aggregate", &PrivateKey::Aggregate, py::call_guard()) + .def(py::self == py::self, py::call_guard()) + .def(py::self != py::self, py::call_guard()) .def("__repr__", [](const PrivateKey &k) { - py::gil_scoped_release release; + RelicGuard guard; uint8_t *output = Util::SecAlloc(PrivateKey::PRIVATE_KEY_SIZE); k.Serialize(output); std::string ret = @@ -141,7 +159,7 @@ PYBIND11_MODULE(dashbls, m) const uint8_t *input = reinterpret_cast(str.data()); uint8_t output[BLS::MESSAGE_HASH_LEN]; { - py::gil_scoped_release release; + RelicGuard guard; Util::Hash256(output, (const uint8_t *)str.data(), str.size()); } return py::bytes( @@ -150,38 +168,38 @@ PYBIND11_MODULE(dashbls, m) py::class_(m, "BasicSchemeMPL") .def("sk_to_g1", [](const PrivateKey &seckey){ - py::gil_scoped_release release; + RelicGuard guard; return BasicSchemeMPL().SkToG1(seckey); }) .def( "key_gen", [](const py::bytes &b) { std::string str(b); - py::gil_scoped_release release; + RelicGuard guard; const vector inputVec(str.begin(), str.end()); return BasicSchemeMPL().KeyGen(inputVec); }) .def("derive_child_sk", [](const PrivateKey& sk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return BasicSchemeMPL().DeriveChildSk(sk, index); }) .def("derive_child_sk_unhardened", [](const PrivateKey& sk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return BasicSchemeMPL().DeriveChildSkUnhardened(sk, index); }) .def("derive_child_pk_unhardened", [](const G1Element& pk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return BasicSchemeMPL().DeriveChildPkUnhardened(pk, index); }) .def("aggregate", [](const vector &signatures) { - py::gil_scoped_release release; + RelicGuard guard; return BasicSchemeMPL().Aggregate(signatures); }) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return BasicSchemeMPL().Sign(pk, v); }) @@ -191,7 +209,7 @@ PYBIND11_MODULE(dashbls, m) const py::bytes &msg, const G2Element &sig) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return BasicSchemeMPL().Verify(pk, v, sig); }) @@ -205,14 +223,14 @@ PYBIND11_MODULE(dashbls, m) std::string s(msgs[i]); vecs[i] = vector(s.begin(), s.end()); } - py::gil_scoped_release release; + RelicGuard guard; return BasicSchemeMPL().AggregateVerify(pks, vecs, sig); }) .def( "g2_from_message", [](const py::bytes &msg) { const auto msg_str = std::string(msg); - py::gil_scoped_release release; + RelicGuard guard; const auto msg_bytes = Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()); return G2Element::FromMessage( msg_bytes, @@ -223,38 +241,38 @@ PYBIND11_MODULE(dashbls, m) py::class_(m, "AugSchemeMPL") .def("sk_to_g1", [](const PrivateKey &seckey){ - py::gil_scoped_release release; + RelicGuard guard; return AugSchemeMPL().SkToG1(seckey); }) .def( "key_gen", [](const py::bytes &b) { std::string str(b); - py::gil_scoped_release release; + RelicGuard guard; const vector inputVec(str.begin(), str.end()); return AugSchemeMPL().KeyGen(inputVec); }) .def("derive_child_sk", [](const PrivateKey& sk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return AugSchemeMPL().DeriveChildSk(sk, index); }) .def("derive_child_sk_unhardened", [](const PrivateKey& sk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return AugSchemeMPL().DeriveChildSkUnhardened(sk, index); }) .def("derive_child_pk_unhardened", [](const G1Element& pk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return AugSchemeMPL().DeriveChildPkUnhardened(pk, index); }) .def("aggregate", [](const vector& signatures) { - py::gil_scoped_release release; + RelicGuard guard; return AugSchemeMPL().Aggregate(signatures); }) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return AugSchemeMPL().Sign(pk, v); }) @@ -264,7 +282,7 @@ PYBIND11_MODULE(dashbls, m) const py::bytes &msg, const G1Element &prepend_pk) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return AugSchemeMPL().Sign(pk, v, prepend_pk); }) @@ -274,7 +292,7 @@ PYBIND11_MODULE(dashbls, m) const py::bytes &msg, const G2Element &sig) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return AugSchemeMPL().Verify(pk, v, sig); }) @@ -288,14 +306,14 @@ PYBIND11_MODULE(dashbls, m) std::string s(msgs[i]); vecs[i] = vector(s.begin(), s.end()); } - py::gil_scoped_release release; + RelicGuard guard; return AugSchemeMPL().AggregateVerify(pks, vecs, sig); }) .def( "g2_from_message", [](const py::bytes &msg) { const auto msg_str = std::string(msg); - py::gil_scoped_release release; + RelicGuard guard; const auto msg_bytes = Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()); return G2Element::FromMessage( msg_bytes, @@ -306,38 +324,38 @@ PYBIND11_MODULE(dashbls, m) py::class_(m, "PopSchemeMPL") .def("sk_to_g1", [](const PrivateKey &seckey){ - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().SkToG1(seckey); }) .def( "key_gen", [](const py::bytes &b) { std::string str(b); - py::gil_scoped_release release; + RelicGuard guard; const vector inputVec(str.begin(), str.end()); return PopSchemeMPL().KeyGen(inputVec); }) .def("derive_child_sk", [](const PrivateKey& sk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().DeriveChildSk(sk, index); }) .def("derive_child_sk_unhardened", [](const PrivateKey& sk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().DeriveChildSkUnhardened(sk, index); }) .def("derive_child_pk_unhardened", [](const G1Element& pk, uint32_t index){ - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().DeriveChildPkUnhardened(pk, index); }) .def("aggregate", [](const vector& signatures) { - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().Aggregate(signatures); }) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return PopSchemeMPL().Sign(pk, v); }) @@ -347,7 +365,7 @@ PYBIND11_MODULE(dashbls, m) const py::bytes &msg, const G2Element &sig) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return PopSchemeMPL().Verify(pk, v, sig); }) @@ -361,14 +379,14 @@ PYBIND11_MODULE(dashbls, m) std::string s(msgs[i]); vecs[i] = vector(s.begin(), s.end()); } - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().AggregateVerify(pks, vecs, sig); }) .def( "g2_from_message", [](const py::bytes &msg) { const auto msg_str = std::string(msg); - py::gil_scoped_release release; + RelicGuard guard; const auto msg_bytes = Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()); return G2Element::FromMessage( msg_bytes, @@ -377,11 +395,11 @@ PYBIND11_MODULE(dashbls, m) ); }) .def("pop_prove", [](const PrivateKey& privateKey){ - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().PopProve(privateKey); }) .def("pop_verify", [](const G1Element& pubkey, const G2Element& signature){ - py::gil_scoped_release release; + RelicGuard guard; return PopSchemeMPL().PopVerify(pubkey, signature); }) .def( @@ -390,7 +408,7 @@ PYBIND11_MODULE(dashbls, m) const py::bytes &msg, const G2Element &sig) { std::string s(msg); - py::gil_scoped_release release; + RelicGuard guard; vector v(s.begin(), s.end()); return PopSchemeMPL().FastAggregateVerify(pks, v, sig); }); @@ -399,10 +417,10 @@ PYBIND11_MODULE(dashbls, m) .def_property_readonly_static( "SIZE", [](py::object self) { return G1Element::SIZE; }) .def(py::init([](){ - py::gil_scoped_release release; + RelicGuard guard; return G1Element(); })) - .def(py::init(&G1Element::FromByteVector), py::call_guard()) + .def(py::init(&G1Element::FromByteVector), py::call_guard()) .def(py::init([](py::int_ pyint) { std::array buffer{}; if (PyLong_AsByteArray( @@ -413,34 +431,35 @@ PYBIND11_MODULE(dashbls, m) 0) < 0) { throw std::invalid_argument("Failed to cast int to G1Element"); } - py::gil_scoped_release release; + RelicGuard guard; return G1Element::FromBytes(buffer); })) .def(py::init([](py::buffer const b) { auto data = CopyBuffer(b, "G1Element::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return G1Element::FromBytes(data); })) .def( "from_bytes", [](py::buffer const b) { auto data = CopyBuffer(b, "G1Element::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return G1Element::FromBytes(data); }) .def( "from_bytes_unchecked", [](py::buffer const b) { auto data = CopyBuffer(b, "G1Element::SIZE"); + RelicGuard guard; return G1Element::FromBytesUnchecked(data); }) - .def("generator", &G1Element::Generator) + .def("generator", &G1Element::Generator, py::call_guard()) .def_static( "from_message", [](const py::bytes &msg, const py::bytes &dst) { const auto msg_str = std::string(msg); const auto dst_str = CopyDst(dst, "G1Element.from_message"); - py::gil_scoped_release release; + RelicGuard guard; return G1Element::FromMessage( Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()), (const uint8_t *)dst_str.c_str(), @@ -448,49 +467,50 @@ PYBIND11_MODULE(dashbls, m) }, py::arg("msg"), py::arg("dst")) - .def("pair", &G1Element::Pair, py::call_guard()) - .def("negate", &G1Element::Negate, py::call_guard()) - .def("get_fingerprint", &G1Element::GetFingerprint, py::call_guard()) + .def("pair", &G1Element::Pair, py::call_guard()) + .def("negate", &G1Element::Negate, py::call_guard()) + .def("get_fingerprint", &G1Element::GetFingerprint, py::call_guard()) - .def(py::self == py::self) - .def(py::self != py::self) + .def(py::self == py::self, py::call_guard()) + .def(py::self != py::self, py::call_guard()) .def( "__deepcopy__", [](const G1Element &g1, const py::object &memo) { + RelicGuard guard; return G1Element(g1); }) .def( "__add__", [](G1Element &self, G1Element &other) { - py::gil_scoped_release release; + RelicGuard guard; return self + other; }, py::is_operator()) .def( "__mul__", [](G1Element &self, const PrivateKey &other) { - py::gil_scoped_release release; + RelicGuard guard; return self * other; }, py::is_operator()) .def( "__rmul__", [](G1Element &self, const PrivateKey &other) { - py::gil_scoped_release release; + RelicGuard guard; return other * self; }, py::is_operator()) .def( "__and__", [](G1Element &self, G2Element &other) { - py::gil_scoped_release release; + RelicGuard guard; return self & other; }, py::is_operator()) .def( "__repr__", [](const G1Element &ele) { - py::gil_scoped_release release; + RelicGuard guard; std::stringstream s; s << ele; return ""; @@ -498,7 +518,7 @@ PYBIND11_MODULE(dashbls, m) .def( "__str__", [](const G1Element &ele) { - py::gil_scoped_release release; + RelicGuard guard; std::stringstream s; s << ele; return s.str(); @@ -508,7 +528,7 @@ PYBIND11_MODULE(dashbls, m) [](const G1Element &ele) { vector out; { - py::gil_scoped_release release; + RelicGuard guard; out = ele.Serialize(); } py::bytes ans = py::bytes( @@ -516,6 +536,7 @@ PYBIND11_MODULE(dashbls, m) return ans; }) .def("__deepcopy__", [](const G1Element &ele, const py::object &memo) { + RelicGuard guard; return G1Element(ele); }); @@ -523,12 +544,13 @@ PYBIND11_MODULE(dashbls, m) .def_property_readonly_static( "SIZE", [](py::object self) { return G2Element::SIZE; }) .def(py::init([](){ + RelicGuard guard; return G2Element(); })) - .def(py::init(&G2Element::FromByteVector), py::call_guard()) + .def(py::init(&G2Element::FromByteVector), py::call_guard()) .def(py::init([](py::buffer const b) { auto data = CopyBuffer(b, "G2Element::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return G2Element::FromBytes(data); })) .def(py::init([](py::int_ pyint) { @@ -541,29 +563,30 @@ PYBIND11_MODULE(dashbls, m) 0) < 0) { throw std::invalid_argument("Failed to cast int to G2Element"); } - py::gil_scoped_release release; + RelicGuard guard; return G2Element::FromBytes(buffer); })) .def( "from_bytes", [](py::buffer const b) { auto data = CopyBuffer(b, "G2Element::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return G2Element::FromBytes(data); }) .def( "from_bytes_unchecked", [](py::buffer const b) { auto data = CopyBuffer(b, "G2Element::SIZE"); + RelicGuard guard; return G2Element::FromBytesUnchecked(data); }) - .def("generator", &G2Element::Generator) + .def("generator", &G2Element::Generator, py::call_guard()) .def_static( "from_message", [](const py::bytes &msg, const py::bytes &dst) { const auto msg_str = std::string(msg); const auto dst_str = CopyDst(dst, "G2Element.from_message"); - py::gil_scoped_release release; + RelicGuard guard; return G2Element::FromMessage( Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()), (const uint8_t *)dst_str.c_str(), @@ -571,34 +594,35 @@ PYBIND11_MODULE(dashbls, m) }, py::arg("msg"), py::arg("dst")) - .def("pair", &G2Element::Pair, py::call_guard()) - .def("negate", &G2Element::Negate, py::call_guard()) + .def("pair", &G2Element::Pair, py::call_guard()) + .def("negate", &G2Element::Negate, py::call_guard()) .def( "__deepcopy__", [](const G2Element &g2, const py::object &memo) { + RelicGuard guard; return G2Element(g2); }) - .def(py::self == py::self) - .def(py::self != py::self) + .def(py::self == py::self, py::call_guard()) + .def(py::self != py::self, py::call_guard()) .def( "__add__", [](G2Element &self, G2Element &other) { - py::gil_scoped_release release; + RelicGuard guard; return self + other; }, py::is_operator()) .def( "__mul__", [](G2Element &self, const PrivateKey &other) { - py::gil_scoped_release release; + RelicGuard guard; return self * other; }, py::is_operator()) .def( "__rmul__", [](G2Element &self, const PrivateKey &other) { - py::gil_scoped_release release; + RelicGuard guard; return other * self; }, py::is_operator()) @@ -606,7 +630,7 @@ PYBIND11_MODULE(dashbls, m) .def( "__repr__", [](const G2Element &ele) { - py::gil_scoped_release release; + RelicGuard guard; std::stringstream s; s << ele; return ""; @@ -614,7 +638,7 @@ PYBIND11_MODULE(dashbls, m) .def( "__str__", [](const G2Element &ele) { - py::gil_scoped_release release; + RelicGuard guard; std::stringstream s; s << ele; return s.str(); @@ -624,7 +648,7 @@ PYBIND11_MODULE(dashbls, m) [](const G2Element &ele) { vector out; { - py::gil_scoped_release release; + RelicGuard guard; out = ele.Serialize(); } py::bytes ans = py::bytes( @@ -632,16 +656,17 @@ PYBIND11_MODULE(dashbls, m) return ans; }) .def("__deepcopy__", [](const G2Element &ele, const py::object &memo) { + RelicGuard guard; return G2Element(ele); }); py::class_(m, "GTElement") .def_property_readonly_static( "SIZE", [](py::object self) { return GTElement::SIZE; }) - .def(py::init(>Element::FromByteVector), py::call_guard()) + .def(py::init(>Element::FromByteVector), py::call_guard()) .def(py::init([](py::buffer const b) { auto data = CopyBuffer(b, "GTElement::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return GTElement::FromBytes(data); })) .def(py::init([](py::int_ pyint) { @@ -654,35 +679,36 @@ PYBIND11_MODULE(dashbls, m) 0) < 0) { throw std::invalid_argument("Failed to cast int to GTElement"); } - py::gil_scoped_release release; + RelicGuard guard; return GTElement::FromBytes(buffer); })) .def( "from_bytes", [](py::buffer const b) { auto data = CopyBuffer(b, "GTElement::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return GTElement::FromBytes(data); }) .def( "from_bytes_unchecked", [](py::buffer const b) { auto data = CopyBuffer(b, "GTElement::SIZE"); - py::gil_scoped_release release; + RelicGuard guard; return GTElement::FromBytesUnchecked(data); }) - .def("unity", >Element::Unity) - .def(py::self == py::self) - .def(py::self != py::self) + .def("unity", >Element::Unity, py::call_guard()) + .def(py::self == py::self, py::call_guard()) + .def(py::self != py::self, py::call_guard()) .def( "__deepcopy__", [](const GTElement >, const py::object &memo) { + RelicGuard guard; return GTElement(gt); }) .def( "__repr__", [](const GTElement &ele) { - py::gil_scoped_release release; + RelicGuard guard; std::stringstream s; s << ele; return ""; @@ -690,7 +716,7 @@ PYBIND11_MODULE(dashbls, m) .def( "__str__", [](const GTElement &ele) { - py::gil_scoped_release release; + RelicGuard guard; std::stringstream s; s << ele; return s.str(); @@ -700,7 +726,7 @@ PYBIND11_MODULE(dashbls, m) [](const GTElement &ele) { uint8_t *out = new uint8_t[GTElement::SIZE]; { - py::gil_scoped_release release; + RelicGuard guard; ele.Serialize(out); } py::bytes ans = @@ -711,11 +737,12 @@ PYBIND11_MODULE(dashbls, m) .def( "__mul__", [](GTElement &self, GTElement &other) { - py::gil_scoped_release release; + RelicGuard guard; return self * other; }, py::is_operator()) .def("__deepcopy__", [](const GTElement &ele, const py::object &memo) { + RelicGuard guard; return GTElement(ele); }); From bfdd033d84e4e2c7668a15b55c2dd2e4d1abda67 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:49:22 +0530 Subject: [PATCH 26/31] fix: reject oversized from_message messages --- binds/python/pythonbindings.cpp | 149 +++++++++++++++++++------------- binds/python/test_unit.py | 74 +++++++++++++++- 2 files changed, 160 insertions(+), 63 deletions(-) diff --git a/binds/python/pythonbindings.cpp b/binds/python/pythonbindings.cpp index 76c3be58c..b037a74d6 100644 --- a/binds/python/pythonbindings.cpp +++ b/binds/python/pythonbindings.cpp @@ -21,8 +21,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -59,19 +61,59 @@ struct RelicGuard { std::lock_guard lock; }; -// md_xmd caps a tag at 255 bytes but compares signed, so a tag at or beyond 2 GiB -// truncates negative, slips the guard and is widened back to a huge length. -std::string CopyDst(const py::bytes &dst, const char *who) +// Measure before copying, so an oversized input fails as ValueError instead of +// bad_alloc, and measure the header rather than py::len, because a subclass may +// override __len__ while the copy below still takes the backing payload. +std::string CopyChecked(const py::bytes &b, size_t limit, const char *who, const char *what) { - // Measure before copying: the tags this rejects are precisely the ones too - // large to want a second copy of. - const auto size = py::len(dst); - if (size > 255) { + const auto signed_size = PyBytes_Size(b.ptr()); + if (signed_size < 0) { + throw py::error_already_set(); + } + const auto size = static_cast(signed_size); + if (size > limit) { throw std::invalid_argument( - std::string(who) + ": domain separation tag must be at most 255 bytes, got " + - std::to_string(size)); + std::string(who) + ": " + what + " must be at most " + std::to_string(limit) + + " bytes, got " + std::to_string(size)); } - return std::string(dst); + return std::string(b); +} + +// md_xmd caps a tag at 255 bytes but compares signed, so a tag at or beyond +// 2 GiB truncates negative, slips the guard and is widened back to a huge +// length. +std::string CopyDst(const py::bytes &dst, const char *who) +{ + return CopyChecked(dst, 255, who, "domain separation tag"); +} + +// ep_map_dst takes the message length as an int and md_xmd checks only the +// output and tag lengths, so nothing on relic's side catches a message at or +// beyond 2 GiB narrowing negative on the way in. +std::string CopyMsg(const py::bytes &msg, const char *who) +{ + return CopyChecked(msg, std::numeric_limits::max(), who, "message"); +} + +// The augmented schemes prepend a serialized G1Element and hand the result to +// the same int-typed length, so the message has to leave room for the prefix. +std::string CopyAugMsg(const py::bytes &msg, const char *who) +{ + return CopyChecked( + msg, std::numeric_limits::max() - G1Element::SIZE, who, "message"); +} + +// relic is built ALLOC=AUTO, so md_hmac stages the seed on the stack and a few +// MiB of it takes the process down. Nothing near this cap is a legitimate IKM +// (the spec floor is 32 bytes), so keep it far below any thread's stack. +std::string CopySeed(const py::bytes &seed, const char *who) +{ + return CopyChecked(seed, 64 * 1024, who, "seed"); +} + +std::vector ToVec(const std::string &s) +{ + return std::vector(s.begin(), s.end()); } // Bytes is a pointer and a length, so a strided view has no counterpart to @@ -155,8 +197,8 @@ PYBIND11_MODULE(dashbls, m) }); py::class_(m, "Util").def("hash256", [](const py::bytes &message) { - std::string str(message); - const uint8_t *input = reinterpret_cast(str.data()); + // Hash256 takes a size_t but md_map_sh256 narrows it to an int + const auto str = CopyMsg(message, "Util.hash256"); uint8_t output[BLS::MESSAGE_HASH_LEN]; { RelicGuard guard; @@ -174,10 +216,9 @@ PYBIND11_MODULE(dashbls, m) .def( "key_gen", [](const py::bytes &b) { - std::string str(b); + const auto str = CopySeed(b, "BasicSchemeMPL.key_gen"); RelicGuard guard; - const vector inputVec(str.begin(), str.end()); - return BasicSchemeMPL().KeyGen(inputVec); + return BasicSchemeMPL().KeyGen(ToVec(str)); }) .def("derive_child_sk", [](const PrivateKey& sk, uint32_t index){ RelicGuard guard; @@ -198,20 +239,18 @@ PYBIND11_MODULE(dashbls, m) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg) { - std::string s(msg); + const auto s = CopyMsg(msg, "BasicSchemeMPL.sign"); RelicGuard guard; - vector v(s.begin(), s.end()); - return BasicSchemeMPL().Sign(pk, v); + return BasicSchemeMPL().Sign(pk, ToVec(s)); }) .def( "verify", [](const G1Element &pk, const py::bytes &msg, const G2Element &sig) { - std::string s(msg); + const auto s = CopyMsg(msg, "BasicSchemeMPL.verify"); RelicGuard guard; - vector v(s.begin(), s.end()); - return BasicSchemeMPL().Verify(pk, v, sig); + return BasicSchemeMPL().Verify(pk, ToVec(s), sig); }) .def( "aggregate_verify", @@ -219,9 +258,9 @@ PYBIND11_MODULE(dashbls, m) const vector &msgs, const G2Element &sig) { vector> vecs(msgs.size()); - for (int i = 0; i < (int)msgs.size(); ++i) { - std::string s(msgs[i]); - vecs[i] = vector(s.begin(), s.end()); + for (size_t i = 0; i < msgs.size(); ++i) { + vecs[i] = ToVec( + CopyMsg(msgs[i], "BasicSchemeMPL.aggregate_verify")); } RelicGuard guard; return BasicSchemeMPL().AggregateVerify(pks, vecs, sig); @@ -229,7 +268,7 @@ PYBIND11_MODULE(dashbls, m) .def( "g2_from_message", [](const py::bytes &msg) { - const auto msg_str = std::string(msg); + const auto msg_str = CopyMsg(msg, "BasicSchemeMPL.g2_from_message"); RelicGuard guard; const auto msg_bytes = Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()); return G2Element::FromMessage( @@ -247,10 +286,9 @@ PYBIND11_MODULE(dashbls, m) .def( "key_gen", [](const py::bytes &b) { - std::string str(b); + const auto str = CopySeed(b, "AugSchemeMPL.key_gen"); RelicGuard guard; - const vector inputVec(str.begin(), str.end()); - return AugSchemeMPL().KeyGen(inputVec); + return AugSchemeMPL().KeyGen(ToVec(str)); }) .def("derive_child_sk", [](const PrivateKey& sk, uint32_t index){ RelicGuard guard; @@ -271,30 +309,27 @@ PYBIND11_MODULE(dashbls, m) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg) { - std::string s(msg); + const auto s = CopyAugMsg(msg, "AugSchemeMPL.sign"); RelicGuard guard; - vector v(s.begin(), s.end()); - return AugSchemeMPL().Sign(pk, v); + return AugSchemeMPL().Sign(pk, ToVec(s)); }) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg, const G1Element &prepend_pk) { - std::string s(msg); + const auto s = CopyAugMsg(msg, "AugSchemeMPL.sign"); RelicGuard guard; - vector v(s.begin(), s.end()); - return AugSchemeMPL().Sign(pk, v, prepend_pk); + return AugSchemeMPL().Sign(pk, ToVec(s), prepend_pk); }) .def( "verify", [](const G1Element &pk, const py::bytes &msg, const G2Element &sig) { - std::string s(msg); + const auto s = CopyAugMsg(msg, "AugSchemeMPL.verify"); RelicGuard guard; - vector v(s.begin(), s.end()); - return AugSchemeMPL().Verify(pk, v, sig); + return AugSchemeMPL().Verify(pk, ToVec(s), sig); }) .def( "aggregate_verify", @@ -302,9 +337,9 @@ PYBIND11_MODULE(dashbls, m) const vector &msgs, const G2Element &sig) { vector> vecs(msgs.size()); - for (int i = 0; i < (int)msgs.size(); ++i) { - std::string s(msgs[i]); - vecs[i] = vector(s.begin(), s.end()); + for (size_t i = 0; i < msgs.size(); ++i) { + vecs[i] = ToVec( + CopyAugMsg(msgs[i], "AugSchemeMPL.aggregate_verify")); } RelicGuard guard; return AugSchemeMPL().AggregateVerify(pks, vecs, sig); @@ -312,7 +347,7 @@ PYBIND11_MODULE(dashbls, m) .def( "g2_from_message", [](const py::bytes &msg) { - const auto msg_str = std::string(msg); + const auto msg_str = CopyMsg(msg, "AugSchemeMPL.g2_from_message"); RelicGuard guard; const auto msg_bytes = Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()); return G2Element::FromMessage( @@ -330,10 +365,9 @@ PYBIND11_MODULE(dashbls, m) .def( "key_gen", [](const py::bytes &b) { - std::string str(b); + const auto str = CopySeed(b, "PopSchemeMPL.key_gen"); RelicGuard guard; - const vector inputVec(str.begin(), str.end()); - return PopSchemeMPL().KeyGen(inputVec); + return PopSchemeMPL().KeyGen(ToVec(str)); }) .def("derive_child_sk", [](const PrivateKey& sk, uint32_t index){ RelicGuard guard; @@ -354,20 +388,18 @@ PYBIND11_MODULE(dashbls, m) .def( "sign", [](const PrivateKey &pk, const py::bytes &msg) { - std::string s(msg); + const auto s = CopyMsg(msg, "PopSchemeMPL.sign"); RelicGuard guard; - vector v(s.begin(), s.end()); - return PopSchemeMPL().Sign(pk, v); + return PopSchemeMPL().Sign(pk, ToVec(s)); }) .def( "verify", [](const G1Element &pk, const py::bytes &msg, const G2Element &sig) { - std::string s(msg); + const auto s = CopyMsg(msg, "PopSchemeMPL.verify"); RelicGuard guard; - vector v(s.begin(), s.end()); - return PopSchemeMPL().Verify(pk, v, sig); + return PopSchemeMPL().Verify(pk, ToVec(s), sig); }) .def( "aggregate_verify", @@ -375,9 +407,9 @@ PYBIND11_MODULE(dashbls, m) const vector &msgs, const G2Element &sig) { vector> vecs(msgs.size()); - for (int i = 0; i < (int)msgs.size(); ++i) { - std::string s(msgs[i]); - vecs[i] = vector(s.begin(), s.end()); + for (size_t i = 0; i < msgs.size(); ++i) { + vecs[i] = + ToVec(CopyMsg(msgs[i], "PopSchemeMPL.aggregate_verify")); } RelicGuard guard; return PopSchemeMPL().AggregateVerify(pks, vecs, sig); @@ -385,7 +417,7 @@ PYBIND11_MODULE(dashbls, m) .def( "g2_from_message", [](const py::bytes &msg) { - const auto msg_str = std::string(msg); + const auto msg_str = CopyMsg(msg, "PopSchemeMPL.g2_from_message"); RelicGuard guard; const auto msg_bytes = Bytes((const uint8_t *)msg_str.c_str(), msg_str.size()); return G2Element::FromMessage( @@ -407,10 +439,9 @@ PYBIND11_MODULE(dashbls, m) [](const vector &pks, const py::bytes &msg, const G2Element &sig) { - std::string s(msg); + const auto s = CopyMsg(msg, "PopSchemeMPL.fast_aggregate_verify"); RelicGuard guard; - vector v(s.begin(), s.end()); - return PopSchemeMPL().FastAggregateVerify(pks, v, sig); + return PopSchemeMPL().FastAggregateVerify(pks, ToVec(s), sig); }); py::class_(m, "G1Element") @@ -457,7 +488,7 @@ PYBIND11_MODULE(dashbls, m) .def_static( "from_message", [](const py::bytes &msg, const py::bytes &dst) { - const auto msg_str = std::string(msg); + const auto msg_str = CopyMsg(msg, "G1Element.from_message"); const auto dst_str = CopyDst(dst, "G1Element.from_message"); RelicGuard guard; return G1Element::FromMessage( @@ -584,7 +615,7 @@ PYBIND11_MODULE(dashbls, m) .def_static( "from_message", [](const py::bytes &msg, const py::bytes &dst) { - const auto msg_str = std::string(msg); + const auto msg_str = CopyMsg(msg, "G2Element.from_message"); const auto dst_str = CopyDst(dst, "G2Element.from_message"); RelicGuard guard; return G2Element::FromMessage( diff --git a/binds/python/test_unit.py b/binds/python/test_unit.py index c4733a5c6..2a28c56b3 100755 --- a/binds/python/test_unit.py +++ b/binds/python/test_unit.py @@ -22,6 +22,7 @@ GTElement, PopSchemeMPL, PrivateKey, + Util, ) # fmt: off @@ -36,6 +37,12 @@ SCHEMES = (BasicSchemeMPL, AugSchemeMPL, PopSchemeMPL) SCHEME_IDS = [scheme.__name__ for scheme in SCHEMES] +G1_DST = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" +G2_DST = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_" + +# Keeps md_hmac's alloca of the seed off the stack, see CopySeed in the binds +SEED_LIMIT = 64 * 1024 + def _derive_two_keypairs() -> tuple[PrivateKey, G1Element, PrivateKey, G1Element]: seed1 = bytes([1]) + SEED[1:] @@ -287,10 +294,6 @@ def test_aggregate_verify_zero_items() -> None: assert AugSchemeMPL.aggregate_verify([], [], G2Element()) -G1_DST = b"BLS_SIG_BLS12381G1_XMD:SHA-256_SSWU_RO_NUL_" -G2_DST = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_" - - def test_from_message() -> None: msg = bytes([10]) * 32 assert G2Element.from_message(msg, G2_DST) == BasicSchemeMPL.g2_from_message(msg) @@ -318,6 +321,61 @@ def test_from_message_accepts_maximum_dst() -> None: assert G2Element.from_message(bytes([10]) * 32, b"x" * 255) != G2Element() +class _LyingBytes(bytes): + """A bytes subclass whose __len__ disagrees with its payload.""" + + def __init__(self, payload: bytes, claimed_len: int) -> None: + del payload + self._claimed_len = claimed_len + + def __new__(cls, payload: bytes, claimed_len: int) -> "_LyingBytes": + del claimed_len + return super().__new__(cls, payload) + + def __len__(self) -> int: + return self._claimed_len + + +@pytest.mark.parametrize("element", [G1Element, G2Element], ids=["G1Element", "G2Element"]) +def test_from_message_measures_the_dst_payload_not_its_len(element: type) -> None: + """The length check has to read the same bytes the copy takes, or a subclass + that understates __len__ walks an oversized tag straight into relic.""" + dst = _LyingBytes(b"x" * 256, 1) + assert len(dst) == 1 + with pytest.raises(ValueError): + element.from_message(bytes([10]) * 32, dst) + + # and the mirror case: a small tag must not be rejected for lying upwards + honest = G1_DST if element is G1Element else G2_DST + assert element.from_message(bytes([10]) * 32, _LyingBytes(honest, 300)) == element.from_message( + bytes([10]) * 32, honest + ) + + +def test_length_checked_paths_use_the_payload_of_a_bytes_subclass() -> None: + """Every entry point that measures a length before copying must agree with + the payload it ends up hashing, whatever __len__ claims.""" + assert Util.hash256(_LyingBytes(b"abc", 1 << 40)) == Util.hash256(b"abc") + + seed = bytes([9]) * 32 + for scheme in (BasicSchemeMPL, AugSchemeMPL, PopSchemeMPL): + sk = scheme.key_gen(_LyingBytes(seed, 1)) + assert sk == scheme.key_gen(seed) + msg = bytes([1, 2, 3]) + assert scheme.sign(sk, _LyingBytes(msg, 1 << 40)) == scheme.sign(sk, msg) + assert scheme.verify(sk.get_g1(), _LyingBytes(msg, 1), scheme.sign(sk, msg)) + + +@pytest.mark.parametrize("scheme", SCHEMES, ids=SCHEME_IDS) +def test_key_gen_rejects_oversized_seeds(scheme: type) -> None: + """relic is built ALLOC=AUTO, so md_hmac stages the seed with alloca and a + seed of a few MiB would take the process down instead of raising.""" + with pytest.raises(ValueError): + scheme.key_gen(b"x" * (SEED_LIMIT + 1)) + # the cap itself has to keep working + assert scheme.key_gen(b"x" * SEED_LIMIT) == scheme.key_gen(b"x" * SEED_LIMIT) + + def test_from_bytes_and_from_bytes_unchecked_agree_on_valid_point() -> None: sk1 = BasicSchemeMPL.key_gen(b"1" * 32) good_point_bytes = bytes(sk1.get_g1()) @@ -372,3 +430,11 @@ def test_from_bytes_rejects_non_contiguous_buffers(cls: type, size: int) -> None def test_from_bytes_unchecked_rejects_non_contiguous_buffers(cls: type, size: int) -> None: with pytest.raises(BufferError): cls.from_bytes_unchecked(memoryview(bytearray(size))[::-1]) + + +@pytest.mark.parametrize("element", [G1Element, G2Element], ids=["G1Element", "G2Element"]) +def test_from_message_accepts_a_large_message(element: type) -> None: + # ep_map_dst takes the message length as an int and relic checks nothing on + # its side, so the binds guard it. Anything under the limit has to keep + # working; the limit itself is not testable without ~2 GiB of memory. + assert element.from_message(b"x" * (1 << 20), G1_DST) != element() From b0cf33289748614cf98d18fbee63d4287bbe18d3 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:53:21 +0530 Subject: [PATCH 27/31] ci: split binds into per-language reusable workflows --- .github/workflows/binds-go.yml | 56 ++++++++++++++++++++ .github/workflows/binds-js.yml | 88 +++++++++++++++++++++++++++++++ .github/workflows/binds-rs.yml | 49 +++++++++++++++++ .github/workflows/build-binds.yml | 87 ++++++------------------------ 4 files changed, 210 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/binds-go.yml create mode 100644 .github/workflows/binds-js.yml create mode 100644 .github/workflows/binds-rs.yml diff --git a/.github/workflows/binds-go.yml b/.github/workflows/binds-go.yml new file mode 100644 index 000000000..ccda3c190 --- /dev/null +++ b/.github/workflows/binds-go.yml @@ -0,0 +1,56 @@ +name: Binds (Go) + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.os }}, Go ${{ matrix.golang }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-24.04-arm] + golang: [ '1.24' ] + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install Go + uses: actions/setup-go@v7 + with: + go-version: ^${{ matrix.golang }} + + - name: Prepare build system for Ubuntu + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update + sudo apt-get install -qq --yes valgrind libgmp-dev cmake + hash -r + cmake --version + + - name: Prepare build system for macOS + if: startsWith(matrix.os, 'macos') + run: | + brew install gmp + + - name: Build library using CMake + run: | + cores=$(getconf _NPROCESSORS_ONLN) + jobs=$(( cores > 1 ? cores - 1 : 1 )) + mkdir -p build && cd build + cmake .. -DCMAKE_BUILD_TYPE=Debug + cmake --build . --parallel "$jobs" + + - name: Build bindings + run: | + cd go-bindings + make config + make diff --git a/.github/workflows/binds-js.yml b/.github/workflows/binds-js.yml new file mode 100644 index 000000000..1443dff94 --- /dev/null +++ b/.github/workflows/binds-js.yml @@ -0,0 +1,88 @@ +name: Binds (Javascript) + +on: + workflow_call: + secrets: + NPM_TOKEN: + description: Token used to publish the package to the npm registry + required: false + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.os }}, Javascript + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-24.04-arm] + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install Emscripten SDK + uses: mymindstorm/setup-emsdk@v16 + + - name: Build JavaScript bindings + run: | + emcc -v + sh emsdk_build.sh + + - name: Test JavaScript bindings + run: | + sh js_test.sh + + publish: + name: Publish (releases), Javascript + if: startsWith(github.ref, 'refs/tags/') + needs: build + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + + - name: Install Emscripten SDK + uses: mymindstorm/setup-emsdk@v16 + + - name: Set up Node + uses: actions/setup-node@v6 + with: + node-version: '20' + registry-url: https://registry.npmjs.org + + # emsdk_build.sh copies package.json into js_build/, so the version has to + # be rewritten in the source tree before the build runs. + - name: Update version in package.json + working-directory: js-bindings + env: + RELEASE: ${{ github.ref_name }} + run: | + jq --arg VER "${RELEASE#v}" '.version=$VER' package.json > temp.json + mv temp.json package.json + + - name: Build JavaScript bindings + run: | + emcc -v + sh emsdk_build.sh + + - name: Publish to npm + working-directory: js_build/js-bindings + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public diff --git a/.github/workflows/binds-rs.yml b/.github/workflows/binds-rs.yml new file mode 100644 index 000000000..8e3db323d --- /dev/null +++ b/.github/workflows/binds-rs.yml @@ -0,0 +1,49 @@ +name: Binds (Rust) + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + name: ${{ matrix.os }}, Rust ${{ matrix.rust }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [macos-latest, ubuntu-24.04-arm] + rust: [ '1.91.0' ] + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Prepare build system for Ubuntu + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update + sudo apt-get install -qq --yes valgrind libgmp-dev cmake + hash -r + cmake --version + + - name: Prepare build system for macOS + if: startsWith(matrix.os, 'macos') + run: | + brew install gmp + + - name: Install Rust + run: | + rustup toolchain install ${{ matrix.rust }} + rustup default ${{ matrix.rust }} + rustc --version + cargo --version + + - name: Build and test Rust bindings + run: | + cd rust-bindings/bls-dash-sys + cargo test diff --git a/.github/workflows/build-binds.yml b/.github/workflows/build-binds.yml index 3c2dd69ce..a83feaabe 100644 --- a/.github/workflows/build-binds.yml +++ b/.github/workflows/build-binds.yml @@ -11,77 +11,24 @@ on: - '**' concurrency: - # SHA is added to the end if on `main` to let all main workflows run group: ${{ github.ref }}-${{ github.workflow }}-${{ github.event_name }}-${{ (github.ref == 'refs/heads/main') && github.sha || '' }} cancel-in-progress: true -jobs: - build: - name: ${{ matrix.os }}, Go ${{ matrix.golang }}, Rust ${{ matrix.rust }} - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [macos-latest, ubuntu-latest] - golang: [ '1.24' ] - rust: [ '1.91.0' ] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Install Emscripten SDK - uses: mymindstorm/setup-emsdk@v11 - - - name: Build JavaScript bindings - run: | - emcc -v - sh emsdk_build.sh - - - name: Test JavaScript bindings - run: | - sh js_test.sh - - - name: Install Go - uses: actions/setup-go@v2 - with: - go-version: ^${{ matrix.golang }} +permissions: + contents: read - - name: Prepare build system for Ubuntu - if: startsWith(matrix.os, 'ubuntu') - run: | - sudo apt-get update - sudo apt-get install -qq --yes valgrind libgmp-dev cmake - hash -r - cmake --version - - - name: Prepare build system for macOS - if: startsWith(matrix.os, 'macos') - run: | - ls -l - export MACOSX_DEPLOYMENT_TARGET=10.14 - brew install gmp - - - name: Build library using CMake - run: | - mkdir -p build && cd build - cmake .. -DCMAKE_BUILD_TYPE=Debug - cmake --build . -- -j 6 - - - name: Build Go bindings - run: | - cd go-bindings - make config - make - - - name: Install Rust - run: | - rustup toolchain install ${{ matrix.rust }} - rustup default ${{ matrix.rust }} - rustc --version - cargo --version - - - name: Build and test Rust bindings - run: | - cd rust-bindings/bls-dash-sys - cargo test +jobs: + build-go: + name: Go + uses: ./.github/workflows/binds-go.yml + + build-js: + name: JavaScript + uses: ./.github/workflows/binds-js.yml + # Reusable workflows do not inherit secrets; the tagged publish needs this. + secrets: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + build-rs: + name: Rust + uses: ./.github/workflows/binds-rs.yml From 7e170b7423858c7a1838ed85e5942f0411caaeb9 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 18:13:11 +0530 Subject: [PATCH 28/31] fix: sidestep `java.lang.UnsupportedClassVersionError` on Ubuntu runners --- .github/workflows/binds-js.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/binds-js.yml b/.github/workflows/binds-js.yml index 1443dff94..fa9bbf15d 100644 --- a/.github/workflows/binds-js.yml +++ b/.github/workflows/binds-js.yml @@ -26,6 +26,12 @@ jobs: with: persist-credentials: false + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '21' + - name: Install Emscripten SDK uses: mymindstorm/setup-emsdk@v16 From e1df3f3905c8197444b2f3fcdce847ed59108480 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 20:40:39 +0530 Subject: [PATCH 29/31] ci: add a reusable workflow for the python binds --- .github/workflows/binds-py.yml | 69 +++++++++++++++++++++++++++++++ .github/workflows/build-binds.yml | 4 ++ pyproject.toml | 28 ++++++++++++- setup.py | 3 ++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/binds-py.yml diff --git a/.github/workflows/binds-py.yml b/.github/workflows/binds-py.yml new file mode 100644 index 000000000..1a2f2055d --- /dev/null +++ b/.github/workflows/binds-py.yml @@ -0,0 +1,69 @@ +name: Binds (Python) + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + lint: + name: Lint, Python + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + + - name: Check lints + run: uvx ruff check --output-format=github . + + - name: Check formatting + run: uvx ruff format --check --diff . + + build: + name: Build (${{ matrix.name }}), Python + needs: lint + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + include: >- + ${{ fromJSON(startsWith(github.ref, 'refs/tags/') + && '[ + {"name": "linux-arm64", "runs-on": "ubuntu-24.04-arm", "archs": "aarch64"}, + {"name": "linux-amd64", "runs-on": "ubuntu-latest", "archs": "x86_64"}, + {"name": "macos-arm64", "runs-on": "macos-latest", "archs": "arm64", "macos-target": "14.0"}, + {"name": "macos-amd64", "runs-on": "macos-15-intel", "archs": "x86_64", "macos-target": "14.0"}, + {"name": "windows-amd64", "runs-on": "windows-latest", "archs": "AMD64"}, + {"name": "windows-arm64", "runs-on": "windows-11-arm", "archs": "ARM64"} + ]' + || '[ + {"name": "linux-arm64", "runs-on": "ubuntu-24.04-arm", "archs": "aarch64"}, + {"name": "macos-arm64", "runs-on": "macos-latest", "archs": "arm64", "macos-target": "14.0"}, + {"name": "windows-amd64", "runs-on": "windows-latest", "archs": "AMD64"} + ]') }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + with: + enable-cache: false + + - name: Build and test wheels + uses: pypa/cibuildwheel@v4.1.1 + env: + CIBW_ARCHS: ${{ matrix.archs }} + MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos-target }} diff --git a/.github/workflows/build-binds.yml b/.github/workflows/build-binds.yml index a83feaabe..4cc508d45 100644 --- a/.github/workflows/build-binds.yml +++ b/.github/workflows/build-binds.yml @@ -32,3 +32,7 @@ jobs: build-rs: name: Rust uses: ./.github/workflows/binds-rs.yml + + build-py: + name: Python + uses: ./.github/workflows/binds-py.yml diff --git a/pyproject.toml b/pyproject.toml index 6dbfb3fb8..c9b5e5526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,12 +22,38 @@ dev = [ ] [build-system] -requires = ["setuptools>=77", "setuptools_scm>=8", "pybind11>=2.13.6"] +requires = [ + "cmake>=3.18", + "pybind11>=2.13.6", + "setuptools_scm>=8", + "setuptools>=77", +] build-backend = "setuptools.build_meta" [tool.setuptools_scm] local_scheme = "no-local-version" +[tool.cibuildwheel] +build-frontend = "uv" +skip = [ + "*-musllinux_*", + "cp*t-*", + "pp*", +] +test-command = "pytest --import-mode=importlib {project}/binds/python/test_unit.py" +test-requires = [ + "pytest>=8.1", + "pytest-benchmark>=5.1", +] + +[tool.cibuildwheel.linux] +before-all = "dnf -y install gmp-devel" +manylinux-aarch64-image = "manylinux_2_28" +manylinux-x86_64-image = "manylinux_2_28" + +[tool.cibuildwheel.macos] +environment = { CMAKE_ARGS = "-DARITH=easy" } + [tool.pytest.ini_options] testpaths = ["binds/python"] addopts = "--benchmark-skip" diff --git a/setup.py b/setup.py index 382b4647c..cb8f9b6b8 100644 --- a/setup.py +++ b/setup.py @@ -12,6 +12,7 @@ import os import re +import shlex import shutil import subprocess import sys @@ -78,6 +79,8 @@ def build_extension(self, ext: CMakeExtension) -> None: "-DVERSION_INFO=" + self.distribution.get_version(), ] + cmake_args += shlex.split(os.environ.get("CMAKE_ARGS", "")) + try: import pybind11 except ImportError: From cc949d470196f64937d4061a43b60c4e8f4b9a1a Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Mon, 3 Aug 2026 18:42:14 +0530 Subject: [PATCH 30/31] ci: add publish workflow, generate and push PEP 503 index to skip PyPi --- .github/scripts/build_simple_index.py | 104 ++++++++++++++++++++++++++ .github/workflows/binds-py.yml | 87 +++++++++++++++++++++ .github/workflows/build-binds.yml | 4 + .github/workflows/build-docs.yml | 51 +++++++++++++ .gitignore | 3 + README.md | 14 ++++ 6 files changed, 263 insertions(+) create mode 100755 .github/scripts/build_simple_index.py create mode 100644 .github/workflows/build-docs.yml diff --git a/.github/scripts/build_simple_index.py b/.github/scripts/build_simple_index.py new file mode 100755 index 000000000..e5554ed9e --- /dev/null +++ b/.github/scripts/build_simple_index.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 +# coding: latin-1 + +# +# Copyright (c) 2026-present, Microsoft Corporation +# Copyright (c) 2026-present, The Dash Core developers +# SPDX-License-Identifier: MIT +# + +"""Generate a PEP 503 index for released distributions.""" + +import argparse +import json +import os +import re +import subprocess +import sys +from html import escape +from pathlib import Path + +# PEP 503: lowercase, runs of -_. collapsed to a single - +NAME = "dashbls" +NORMALISED = re.sub(r"[-_.]+", "-", NAME).lower() + +# Must match requires-python in pyproject.toml. +REQUIRES_PYTHON = ">=3.10" + + +def releases(repo: str) -> list[dict]: + """Every release of `repo`, each with the name, URL and digest of its assets.""" + out = subprocess.check_output( # noqa: S603 + [ # noqa: S607 + "gh", + "api", + "--paginate", + f"repos/{repo}/releases", + "--jq", + ".[] | select(.draft | not) | {tag: .tag_name, assets: [.assets[] " + "| {name, url: .browser_download_url, digest}]}", + ], + text=True, + ) + return [json.loads(line) for line in out.splitlines() if line.strip()] + + +def anchor(name: str, url: str, digest: str | None) -> str: + """One link, with the hash pip needs to verify what it downloaded. + + The releases API reports a digest as "sha256:", which is the PEP 503 + fragment in all but spelling. Without it pip has no integrity check at all + and --require-hashes has nothing to match against. + """ + if digest and digest.startswith("sha256:"): + url = f"{url}#sha256={digest.removeprefix('sha256:')}" + return ( + f' {escape(name)}
' + ) + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--output", required=True, type=Path) + parser.add_argument( + "--repo", default=os.environ.get("GITHUB_REPOSITORY", "dashpay/bls-signatures") + ) + args = parser.parse_args() + + # Sdists matter as much as wheels here: they are the only thing installable + # on a platform we do not ship a wheel for, and pip will not find one that + # the index does not list. + files = [] + for release in releases(args.repo): + for asset in release["assets"]: + if asset["name"].endswith((".whl", ".tar.gz")): + files.append((asset["name"], asset["url"], asset.get("digest"))) + files.sort() + if not files: + sys.exit("no distributions found on any release; refusing to publish an empty index") + + project = args.output / "pep503" / NORMALISED + project.mkdir(parents=True, exist_ok=True) + + # pypi:repository-version is PEP 629; charset because the filenames are + # written by whatever runner built them. + head = '\n' + anchors = "\n".join(anchor(*dist) for dist in files) + (project / "index.html").write_text( + f"\n{head}\nLinks for {NAME}\n" + f"\n

Links for {NAME}

\n{anchors}\n\n", + encoding="utf-8", + ) + (args.output / "pep503" / "index.html").write_text( + f"\n{head}\nSimple Index\n" + "\n" + f' {NAME}
\n' + "\n", + encoding="utf-8", + ) + print(f"indexed {len(files)} distributions for {NAME}") + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/binds-py.yml b/.github/workflows/binds-py.yml index 1a2f2055d..6884602a1 100644 --- a/.github/workflows/binds-py.yml +++ b/.github/workflows/binds-py.yml @@ -67,3 +67,90 @@ jobs: env: CIBW_ARCHS: ${{ matrix.archs }} MACOSX_DEPLOYMENT_TARGET: ${{ matrix.macos-target }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: wheels-${{ matrix.name }} + path: wheelhouse/*.whl + if-no-files-found: error + + sdist: + name: Build (sdist), Python + needs: lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install uv + uses: astral-sh/setup-uv@v9.0.0 + + - name: Build the sdist + run: uv build --sdist --out-dir dist + + - name: Prepare build system + run: | + sudo apt-get update + sudo apt-get install -qq --yes libgmp-dev + + - name: Install from the sdist and run the unit tests + run: | + uv venv .venv-sdist + uv pip install --python .venv-sdist --no-binary=dashbls dist/*.tar.gz + uv pip install --python .venv-sdist pytest pytest-benchmark + .venv-sdist/bin/python -m pytest --import-mode=importlib -v binds/python/test_unit.py + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: sdist + path: dist/*.tar.gz + if-no-files-found: error + + publish: + name: Publish (releases), Python + if: startsWith(github.ref, 'refs/tags/') + needs: [build, sdist] + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + pattern: wheels-* + merge-multiple: true + path: dist + + - name: Download the sdist + uses: actions/download-artifact@v4 + with: + name: sdist + path: dist + + - name: Attach the wheels and sdist to the release + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE: ${{ github.ref_name }} + run: | + ls -l dist + # Pushing a tag does not create a release, and `upload` will not make one. + gh release view "$RELEASE" >/dev/null 2>&1 \ + || gh release create "$RELEASE" --verify-tag --generate-notes + gh release upload "$RELEASE" dist/*.whl dist/*.tar.gz --clobber + + index: + name: Publish (index), Python + needs: publish + permissions: + contents: read + pages: write + id-token: write + uses: ./.github/workflows/build-docs.yml diff --git a/.github/workflows/build-binds.yml b/.github/workflows/build-binds.yml index 4cc508d45..fa1f69e17 100644 --- a/.github/workflows/build-binds.yml +++ b/.github/workflows/build-binds.yml @@ -35,4 +35,8 @@ jobs: build-py: name: Python + permissions: + contents: write + pages: write + id-token: write uses: ./.github/workflows/binds-py.yml diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml new file mode 100644 index 000000000..4a6580977 --- /dev/null +++ b/.github/workflows/build-docs.yml @@ -0,0 +1,51 @@ +name: Build docs + +on: + workflow_call: + workflow_dispatch: + +concurrency: + group: pages + cancel-in-progress: false + +permissions: + contents: read + pages: write + id-token: write + +jobs: + pep503: + name: Generate PEP503 index + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deploy.outputs.page_url }} + + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Generate index + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + run: python .github/scripts/build_simple_index.py --output _site + + - name: Configure GitHub Pages + uses: actions/configure-pages@v6 + + - name: Upload index + uses: actions/upload-pages-artifact@v5 + with: + path: _site + + - name: Deploy to GitHub Pages + id: deploy + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 28ba8189d..f968c1dbe 100644 --- a/.gitignore +++ b/.gitignore @@ -177,3 +177,6 @@ tempCodeRunnerFile.py # PyPI configuration file .pypirc + +# PEP503 index +_site diff --git a/README.md b/README.md index 17f2ce073..1d22cdfd4 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,20 @@ cmake --build . --parallel 4 ./src/runbench ``` +## Install Python binds + +Releases are published to a [PEP 503](https://peps.python.org/pep-0503/) index hosted on GitHub Pages rather than to +PyPI. + +```sh +pip install --extra-index-url https://dashpay.github.io/bls-signatures/pep503/ dashbls +``` + +> [!IMPORTANT] +> Use `--extra-index-url`, not `--index-url`. Platforms we do not ship a wheel for fall back to the sdist, and building +> it needs `cmake`, `pybind11`, `setuptools_scm` and `setuptools` from PyPI. `--index-url` would replace PyPI with an +> index that carries only `dashbls`, so the build would fail before it started. + ## Build Python binds Our Python binds target Python 3.10 or higher; they depend on the following packages. Sample code is available From fe6fbf77ff5822bbdc78df4968efb56a77d0e5c4 Mon Sep 17 00:00:00 2001 From: Kittywhiskers Van Gogh <63189531+kwvg@users.noreply.github.com> Date: Sun, 2 Aug 2026 22:56:27 +0530 Subject: [PATCH 31/31] build: drop `windows-arm64` from matrix due to relic limitations relic reaches for `_umul128`, `_udiv128` and `__lzcnt64` behind a bare `_MSC_VER` guard, which are AMD64-only. Patching relic is out of the question. --- .github/workflows/binds-py.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/binds-py.yml b/.github/workflows/binds-py.yml index 6884602a1..803fb37fa 100644 --- a/.github/workflows/binds-py.yml +++ b/.github/workflows/binds-py.yml @@ -41,8 +41,7 @@ jobs: {"name": "linux-amd64", "runs-on": "ubuntu-latest", "archs": "x86_64"}, {"name": "macos-arm64", "runs-on": "macos-latest", "archs": "arm64", "macos-target": "14.0"}, {"name": "macos-amd64", "runs-on": "macos-15-intel", "archs": "x86_64", "macos-target": "14.0"}, - {"name": "windows-amd64", "runs-on": "windows-latest", "archs": "AMD64"}, - {"name": "windows-arm64", "runs-on": "windows-11-arm", "archs": "ARM64"} + {"name": "windows-amd64", "runs-on": "windows-latest", "archs": "AMD64"} ]' || '[ {"name": "linux-arm64", "runs-on": "ubuntu-24.04-arm", "archs": "aarch64"},