diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..429b9d4 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +github: +- ofek +custom: +- https://ofek.dev/donate/ +- https://paypal.me/ofeklev diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..eda2b74 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,62 @@ +name: docs + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + # Fetch all history for applying timestamps to every page + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.9' + + - name: Upgrade Python packaging tools + run: pip install --disable-pip-version-check --upgrade pip setuptools wheel + + - name: Install dependencies + run: python -m pip install --upgrade tox + + - name: Build documentation + run: tox -e docs-ci build + + - uses: actions/upload-artifact@v2 + with: + name: documentation + path: site + + publish: + runs-on: ubuntu-latest + + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: + - build + + steps: + - uses: actions/download-artifact@v2 + with: + name: documentation + path: site + + - uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + commit_message: ${{ github.event.head_commit.message }} + # Write .nojekyll at the root, see: + # https://help.github.com/en/github/working-with-github-pages/about-github-pages#static-site-generators + enable_jekyll: false + # Only deploy if there were changes + allow_empty_commit: false diff --git a/.gitignore b/.gitignore index 8d65801..a004851 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ /coincurve.egg-info /build /dist -/docs/build +/site /wheelhouse /pip-wheel-metadata diff --git a/coincurve/context.py b/coincurve/context.py index c88be3d..e22f74e 100644 --- a/coincurve/context.py +++ b/coincurve/context.py @@ -7,7 +7,7 @@ from ._libsecp256k1 import ffi, lib class Context: - def __init__(self, seed: bytes = None, flag=CONTEXT_ALL): + def __init__(self, seed: bytes = None, flag=CONTEXT_ALL, name: str = ''): if flag not in CONTEXT_FLAGS: raise ValueError('{} is an invalid context flag.'.format(flag)) self._lock = Lock() @@ -15,6 +15,8 @@ class Context: self.ctx = ffi.gc(lib.secp256k1_context_create(flag), lib.secp256k1_context_destroy) self.reseed(seed) + self.name = name + def reseed(self, seed: bytes = None): """ Protects against certain possible future side-channel timing attacks. @@ -24,5 +26,8 @@ class Context: res = lib.secp256k1_context_randomize(self.ctx, ffi.new('unsigned char [32]', seed)) assert res == 1 + def __repr__(self): + return self.name or super().__repr__() + -GLOBAL_CONTEXT = Context() +GLOBAL_CONTEXT = Context(name='GLOBAL_CONTEXT') diff --git a/coincurve/ecdsa.py b/coincurve/ecdsa.py index 5dd0b91..dca768c 100644 --- a/coincurve/ecdsa.py +++ b/coincurve/ecdsa.py @@ -36,7 +36,7 @@ def recover(message: bytes, recover_sig, hasher: Hasher = sha256, context: Conte recovered = lib.secp256k1_ecdsa_recover(context.ctx, pubkey, recover_sig, msg_hash) if recovered: return pubkey - raise Exception('failed to recover ECDSA public key') + raise ValueError('failed to recover ECDSA public key') def serialize_recoverable(recover_sig, context: Context = GLOBAL_CONTEXT) -> bytes: diff --git a/coincurve/keys.py b/coincurve/keys.py index e6cacf4..594e074 100644 --- a/coincurve/keys.py +++ b/coincurve/keys.py @@ -5,8 +5,9 @@ from asn1crypto.keys import ECDomainParameters, ECPointBitString, ECPrivateKey, from coincurve.context import GLOBAL_CONTEXT, Context from coincurve.ecdsa import cdata_to_der, der_to_cdata, deserialize_recoverable, recover, serialize_recoverable from coincurve.flags import EC_COMPRESSED, EC_UNCOMPRESSED -from coincurve.types import Hasher +from coincurve.types import Hasher, Nonce from coincurve.utils import ( + DEFAULT_NONCE, bytes_to_int, der_to_pem, get_valid_secret, @@ -20,22 +21,35 @@ from coincurve.utils import ( from ._libsecp256k1 import ffi, lib -DEFAULT_NONCE = (ffi.NULL, ffi.NULL) - class PrivateKey: def __init__(self, secret: bytes = None, context: Context = GLOBAL_CONTEXT): + """ + :param secret: The secret used to initialize the private key. + If not provided or `None`, a new key will be generated. + """ self.secret: bytes = validate_secret(secret) if secret is not None else get_valid_secret() self.context = context self.public_key: PublicKey = PublicKey.from_valid_secret(self.secret, self.context) - def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce=None) -> bytes: + def sign(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes: + """ + Create an ECDSA signature. + + :param message: The message to sign. + :param hasher: The hash function to use, which must return 32 bytes. By default, + the `sha256` algorithm is used. If `None`, no hashing occurs. + :param custom_nonce: Custom nonce data in the form `(nonce_function, input_data)`. + :return: The ECDSA signature. + :raises ValueError: If the message hash was not 32 bytes long, the nonce generation + function failed, or the private key was invalid. + """ msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: raise ValueError('Message hash must be 32 bytes long.') signature = ffi.new('secp256k1_ecdsa_signature *') - nonce_fn, nonce_data = custom_nonce or DEFAULT_NONCE + nonce_fn, nonce_data = custom_nonce signed = lib.secp256k1_ecdsa_sign(self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data) @@ -44,15 +58,27 @@ class PrivateKey: return cdata_to_der(signature, self.context) - def sign_recoverable(self, message, hasher: Hasher = sha256): + def sign_recoverable(self, message: bytes, hasher: Hasher = sha256, custom_nonce: Nonce = DEFAULT_NONCE) -> bytes: + """ + Create a recoverable ECDSA signature. + + :param message: The message to sign. + :param hasher: The hash function to use, which must return 32 bytes. By default, + the `sha256` algorithm is used. If `None`, no hashing occurs. + :param custom_nonce: Custom nonce data in the form `(nonce_function, input_data)`. + :return: The recoverable ECDSA signature. + :raises ValueError: If the message hash was not 32 bytes long, the nonce generation + function failed, or the private key was invalid. + """ msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: raise ValueError('Message hash must be 32 bytes long.') signature = ffi.new('secp256k1_ecdsa_recoverable_signature *') + nonce_fn, nonce_data = custom_nonce signed = lib.secp256k1_ecdsa_sign_recoverable( - self.context.ctx, signature, msg_hash, self.secret, ffi.NULL, ffi.NULL + self.context.ctx, signature, msg_hash, self.secret, nonce_fn, nonce_data ) if not signed: @@ -61,13 +87,29 @@ class PrivateKey: return serialize_recoverable(signature, self.context) def ecdh(self, public_key: bytes) -> bytes: + """ + Compute an EC Diffie-Hellman secret in constant time. + + :param public_key: The formatted public key. + :return: The 32 byte shared secret. + :raises ValueError: If the public key could not be parsed or was invalid. + """ secret = ffi.new('unsigned char [32]') lib.secp256k1_ecdh(self.context.ctx, secret, PublicKey(public_key).public_key, self.secret, ffi.NULL, ffi.NULL) return bytes(ffi.buffer(secret, 32)) - def add(self, scalar: bytes, update=False): + def add(self, scalar: bytes, update: bool = False): + """ + Add a scalar to the private key. + + :param scalar: The scalar with which to add. + :param update: Whether or not to update and return the private key in-place. + :return: The new private key, or the modified private key if `update` is `True`. + :rtype: PrivateKey + :raises ValueError: If the tweak was out of range or the resulting private key was invalid. + """ scalar = pad_scalar(scalar) secret = ffi.new('unsigned char [32]', self.secret) @@ -86,7 +128,15 @@ class PrivateKey: return PrivateKey(secret, self.context) - def multiply(self, scalar: bytes, update=False): + def multiply(self, scalar: bytes, update: bool = False): + """ + Multiply the private key by a scalar. + + :param scalar: The scalar with which to multiply. + :param update: Whether or not to update and return the private key in-place. + :return: The new private key, or the modified private key if `update` is `True`. + :rtype: PrivateKey + """ scalar = validate_secret(scalar) secret = ffi.new('unsigned char [32]', self.secret) @@ -103,15 +153,27 @@ class PrivateKey: return PrivateKey(secret, self.context) def to_hex(self) -> str: + """ + :return: The private key encoded as a hex string. + """ return self.secret.hex() def to_int(self) -> int: + """ + :return: The private key as an integer. + """ return bytes_to_int(self.secret) def to_pem(self) -> bytes: + """ + :return: The private key encoded in PEM format. + """ return der_to_pem(self.to_der()) def to_der(self) -> bytes: + """ + :return: The private key encoded in DER format. + """ pk = ECPrivateKey( { 'version': 'ecPrivkeyVer1', @@ -135,20 +197,44 @@ class PrivateKey: @classmethod def from_hex(cls, hexed: str, context: Context = GLOBAL_CONTEXT): + """ + :param hexed: The private key encoded as a hex string. + :param context: + :return: The private key. + :rtype: PrivateKey + """ return PrivateKey(hex_to_bytes(hexed), context) @classmethod def from_int(cls, num: int, context: Context = GLOBAL_CONTEXT): + """ + :param num: The private key as an integer. + :param context: + :return: The private key. + :rtype: PrivateKey + """ return PrivateKey(int_to_bytes_padded(num), context) @classmethod def from_pem(cls, pem: bytes, context: Context = GLOBAL_CONTEXT): + """ + :param pem: The private key encoded in PEM format. + :param context: + :return: The private key. + :rtype: PrivateKey + """ return PrivateKey( int_to_bytes_padded(PrivateKeyInfo.load(pem_to_der(pem)).native['private_key']['private_key']), context ) @classmethod def from_der(cls, der: bytes, context: Context = GLOBAL_CONTEXT): + """ + :param der: The private key encoded in DER format. + :param context: + :return: The private key. + :rtype: PrivateKey + """ return PrivateKey(int_to_bytes_padded(PrivateKeyInfo.load(der).native['private_key']['private_key']), context) def _update_public_key(self): @@ -163,6 +249,15 @@ class PrivateKey: class PublicKey: def __init__(self, data, context: Context = GLOBAL_CONTEXT): + """ + :param data: The formatted public key. This class supports parsing + compressed (33 bytes, header byte `0x02` or `0x03`), + uncompressed (65 bytes, header byte `0x04`), or + hybrid (65 bytes, header byte `0x06` or `0x07`) format public keys. + :type data: bytes + :param context: + :raises ValueError: If the public key could not be parsed or was invalid. + """ if not isinstance(data, bytes): self.public_key = data else: @@ -179,6 +274,14 @@ class PublicKey: @classmethod def from_secret(cls, secret: bytes, context: Context = GLOBAL_CONTEXT): + """ + Derive a public key from a private key secret. + + :param secret: The private key secret. + :param context: + :return: The public key. + :rtype: PublicKey + """ public_key = ffi.new('secp256k1_pubkey *') created = lib.secp256k1_ec_pubkey_create(context.ctx, public_key, validate_secret(secret)) @@ -205,18 +308,49 @@ class PublicKey: @classmethod def from_point(cls, x: int, y: int, context: Context = GLOBAL_CONTEXT): + """ + Derive a public key from a coordinate point in the form `(x, y)`. + + :param x: + :param y: + :param context: + :return: The public key. + :rtype: PublicKey + """ return PublicKey(b'\x04' + int_to_bytes_padded(x) + int_to_bytes_padded(y), context) @classmethod def from_signature_and_message( - cls, serialized_sig: bytes, message: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT + cls, signature: bytes, message: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT ): + """ + Recover an ECDSA public key from a recoverable signature. + + :param signature: The recoverable ECDSA signature. + :param message: The message that was supposedly signed. + :param hasher: The hash function to use, which must return 32 bytes. By default, + the `sha256` algorithm is used. If `None`, no hashing occurs. + :param context: + :return: The public key that signed the message. + :rtype: PublicKey + :raises ValueError: If the message hash was not 32 bytes long or recovery of the ECDSA public key failed. + """ return PublicKey( - recover(message, deserialize_recoverable(serialized_sig, context=context), hasher=hasher, context=context) + recover(message, deserialize_recoverable(signature, context=context), hasher=hasher, context=context) ) @classmethod def combine_keys(cls, public_keys, context: Context = GLOBAL_CONTEXT): + """ + Add a number of public keys together. + + :param public_keys: A sequence of public keys. + :type public_keys: List[PublicKey] + :param context: + :return: The combined public key. + :rtype: PublicKey + :raises ValueError: If the sum of the public keys was invalid. + """ public_key = ffi.new('secp256k1_pubkey *') combined = lib.secp256k1_ec_pubkey_combine( @@ -228,7 +362,13 @@ class PublicKey: return PublicKey(public_key, context) - def format(self, compressed=True) -> bytes: + def format(self, compressed: bool = True) -> bytes: + """ + Format the public key. + + :param compressed: Whether or to use the compressed format. + :return: The 33 byte formatted public key, or the 65 byte formatted public key if `compressed` is `False`. + """ length = 33 if compressed else 65 serialized = ffi.new('unsigned char [%d]' % length) output_len = ffi.new('size_t *', length) @@ -240,10 +380,21 @@ class PublicKey: return bytes(ffi.buffer(serialized, length)) def point(self) -> Tuple[int, int]: + """ + :return: The public key as a coordinate point. + """ public_key = self.format(compressed=False) return bytes_to_int(public_key[1:33]), bytes_to_int(public_key[33:]) def verify(self, signature: bytes, message: bytes, hasher: Hasher = sha256) -> bool: + """ + :param signature: The ECDSA signature. + :param message: The message that was supposedly signed. + :param hasher: The hash function to use, which must return 32 bytes. By default, + the `sha256` algorithm is used. If `None`, no hashing occurs. + :return: A boolean indicating whether or not the signature is correct. + :raises ValueError: If the message hash was not 32 bytes long or the DER-encoded signature could not be parsed. + """ msg_hash = hasher(message) if hasher is not None else message if len(msg_hash) != 32: raise ValueError('Message hash must be 32 bytes long.') @@ -253,7 +404,16 @@ class PublicKey: # A performance hack to avoid global bool() lookup. return not not verified - def add(self, scalar: bytes, update=False): + def add(self, scalar: bytes, update: bool = False): + """ + Add a scalar to the public key. + + :param scalar: The scalar with which to add. + :param update: Whether or not to update and return the public key in-place. + :return: The new public key, or the modified public key if `update` is `True`. + :rtype: PublicKey + :raises ValueError: If the tweak was out of range or the resulting public key was invalid. + """ scalar = pad_scalar(scalar) new_key = ffi.new('secp256k1_pubkey *', self.public_key[0]) @@ -269,7 +429,15 @@ class PublicKey: return PublicKey(new_key, self.context) - def multiply(self, scalar: bytes, update=False): + def multiply(self, scalar: bytes, update: bool = False): + """ + Multiply the public key by a scalar. + + :param scalar: The scalar with which to multiply. + :param update: Whether or not to update and return the public key in-place. + :return: The new public key, or the modified public key if `update` is `True`. + :rtype: PublicKey + """ scalar = validate_secret(scalar) new_key = ffi.new('secp256k1_pubkey *', self.public_key[0]) @@ -282,7 +450,17 @@ class PublicKey: return PublicKey(new_key, self.context) - def combine(self, public_keys, update=False): + def combine(self, public_keys, update: bool = False): + """ + Add a number of public keys together. + + :param public_keys: A sequence of public keys. + :type public_keys: List[PublicKey] + :param update: Whether or not to update and return the public key in-place. + :return: The combined public key, or the modified public key if `update` is `True`. + :rtype: PublicKey + :raises ValueError: If the sum of the public keys was invalid. + """ new_key = ffi.new('secp256k1_pubkey *') combined = lib.secp256k1_ec_pubkey_combine( diff --git a/coincurve/types.py b/coincurve/types.py index 123baed..1b12ad3 100644 --- a/coincurve/types.py +++ b/coincurve/types.py @@ -1,5 +1,7 @@ import sys -from typing import Optional +from typing import Optional, Tuple + +from ._libsecp256k1 import ffi # https://bugs.python.org/issue42965 if sys.version_info >= (3, 9, 2): @@ -8,3 +10,4 @@ else: from typing import Callable Hasher = Optional[Callable[[bytes], bytes]] +Nonce = Tuple[ffi.CData, ffi.CData] diff --git a/coincurve/utils.py b/coincurve/utils.py index 44e728a..2e5f5cd 100644 --- a/coincurve/utils.py +++ b/coincurve/utils.py @@ -1,6 +1,6 @@ from base64 import b64decode, b64encode from hashlib import sha256 as _sha256 -from os import urandom +from os import environ, urandom from typing import Generator from coincurve.context import GLOBAL_CONTEXT, Context @@ -19,6 +19,30 @@ PEM_HEADER = b'-----BEGIN PRIVATE KEY-----\n' PEM_FOOTER = b'-----END PRIVATE KEY-----\n' +if environ.get('COINCURVE_BUILDING_DOCS') != 'true': + DEFAULT_NONCE = (ffi.NULL, ffi.NULL) + + def sha256(bytestr: bytes) -> bytes: + return _sha256(bytestr).digest() + + +else: # no cov + + class __Nonce(tuple): + def __repr__(self): + return '(ffi.NULL, ffi.NULL)' + + class __HasherSHA256: + def __call__(self, bytestr: bytes) -> bytes: + return _sha256(bytestr).digest() + + def __repr__(self): + return 'sha256' + + DEFAULT_NONCE = __Nonce((ffi.NULL, ffi.NULL)) # type: ignore + sha256 = __HasherSHA256() + + def pad_hex(hexed: str) -> str: # Pad odd-length hex strings. return hexed if not len(hexed) & 1 else f'0{hexed}' @@ -40,10 +64,6 @@ def hex_to_bytes(hexed: str) -> bytes: return pad_scalar(bytes.fromhex(pad_hex(hexed))) -def sha256(bytestr: bytes) -> bytes: - return _sha256(bytestr).digest() - - def chunk_data(data: bytes, size: int) -> Generator[bytes, None, None]: return (data[i : i + size] for i in range(0, len(data), size)) @@ -76,6 +96,17 @@ def validate_secret(secret: bytes) -> bytes: def verify_signature( signature: bytes, message: bytes, public_key: bytes, hasher: Hasher = sha256, context: Context = GLOBAL_CONTEXT ) -> bool: + """ + :param signature: The ECDSA signature. + :param message: The message that was supposedly signed. + :param public_key: The formatted public key. + :param hasher: The hash function to use, which must return 32 bytes. By default, + the `sha256` algorithm is used. If `None`, no hashing occurs. + :param context: + :return: A boolean indicating whether or not the signature is correct. + :raises ValueError: If the public key could not be parsed or was invalid, the message hash was + not 32 bytes long, or the DER-encoded signature could not be parsed. + """ pubkey = ffi.new('secp256k1_pubkey *') pubkey_parsed = lib.secp256k1_ec_pubkey_parse(context.ctx, pubkey, public_key, len(public_key)) diff --git a/docs/.scripts/49_global_refs.py b/docs/.scripts/49_global_refs.py new file mode 100644 index 0000000..80b4626 --- /dev/null +++ b/docs/.scripts/49_global_refs.py @@ -0,0 +1,3 @@ +def patch(lines): + """This ensures that links and abbreviations are always available.""" + lines.extend(('', '--8<-- "refs.txt"', '')) diff --git a/docs/.snippets/abbrs.txt b/docs/.snippets/abbrs.txt new file mode 100644 index 0000000..d09aeb3 --- /dev/null +++ b/docs/.snippets/abbrs.txt @@ -0,0 +1,2 @@ +*[ECDH]: Elliptic-curve Diffie–Hellman +*[PyPI]: Python Package Index diff --git a/docs/.snippets/links.txt b/docs/.snippets/links.txt new file mode 100644 index 0000000..8fc9301 --- /dev/null +++ b/docs/.snippets/links.txt @@ -0,0 +1,5 @@ +[Bitcoin Core]: https://github.com/bitcoin/bitcoin +[ECDH]: https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman +[RFC 6979]: https://tools.ietf.org/html/rfc6979 +[libsecp256k1]: https://github.com/bitcoin-core/secp256k1 +[secp256k1]: https://en.bitcoin.it/wiki/Secp256k1 diff --git a/docs/.snippets/refs.txt b/docs/.snippets/refs.txt new file mode 100644 index 0000000..7c3f254 --- /dev/null +++ b/docs/.snippets/refs.txt @@ -0,0 +1,4 @@ +--8<-- +links.txt +abbrs.txt +--8<-- diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..566d56b --- /dev/null +++ b/docs/api.md @@ -0,0 +1,50 @@ +# Developer Interface + +----- + +All objects are available directly under the root namespace `coincurve`. + +::: coincurve.verify_signature + rendering: + show_root_full_path: false + selection: + docstring_style: restructured-text + +::: coincurve.PrivateKey + rendering: + show_root_full_path: false + selection: + docstring_style: restructured-text + members: + - __init__ + - sign + - sign_recoverable + - ecdh + - add + - multiply + - to_hex + - to_pem + - to_der + - to_int + - from_hex + - from_pem + - from_der + - from_int + +::: coincurve.PublicKey + rendering: + show_root_full_path: false + selection: + docstring_style: restructured-text + members: + - __init__ + - verify + - format + - point + - combine + - add + - multiply + - combine_keys + - from_signature_and_message + - from_secret + - from_point diff --git a/docs/assets/css/custom.css b/docs/assets/css/custom.css new file mode 100644 index 0000000..7e261a5 --- /dev/null +++ b/docs/assets/css/custom.css @@ -0,0 +1,61 @@ +/* https://github.com/squidfunk/mkdocs-material/issues/1522 */ +.md-typeset h5 { + color: var(--md-default-fg-color); + text-transform: none; +} + +/* https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/ */ +.md-typeset .tabbed-set { + border: 1px solid #eee; +} +.md-typeset .tabbed-content { + padding: 0.5em 1em; +} + +/* Brighter links for dark mode */ +[data-md-color-scheme=slate] { + --md-text-link-color: var(--md-primary-fg-color--light); +} + +/* FiraCode https://github.com/tonsky/FiraCode */ +code { font-family: 'Fira Code', monospace; } + +@supports (font-variation-settings: normal) { + code { font-family: 'Fira Code VF', monospace; } +} + +/* Everything below is from https://pawamoy.github.io/mkdocstrings/handlers/python/#recommended-style-material */ + +/* Indentation. */ +div.doc-contents:not(.first) { + padding-left: 25px; + border-left: 4px solid rgba(230, 230, 230); + margin-bottom: 80px; +} + +/* Don't capitalize names. */ +h5.doc-heading { + text-transform: none !important; +} + +/* Don't use vertical space on hidden ToC entries. */ +.hidden-toc::before { + margin-top: 0 !important; + padding-top: 0 !important; +} + +/* Don't show permalink of hidden ToC entries. */ +.hidden-toc a.headerlink { + display: none; +} + +/* Avoid breaking parameters name, etc. in table cells. */ +td code { + word-break: normal !important; +} + +/* For pieces of Markdown rendered in table cells. */ +td p { + margin-top: 0 !important; + margin-bottom: 0 !important; +} diff --git a/docs/assets/images/favicon.ico b/docs/assets/images/favicon.ico new file mode 100644 index 0000000..1042619 Binary files /dev/null and b/docs/assets/images/favicon.ico differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..009f726 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,46 @@ +# coincurve + +[![CI - Build](https://github.com/ofek/coincurve/workflows/build/badge.svg)](https://github.com/ofek/coincurve/actions?query=workflow%3Abuild) +[![CI - Docs](https://github.com/ofek/coincurve/workflows/docs/badge.svg)](https://github.com/ofek/coincurve/actions?query=workflow%3Adocs) +[![CI - Coverage](https://img.shields.io/codecov/c/github/ofek/coincurve/master.svg?logo=codecov&logoColor=red)](https://codecov.io/github/ofek/coincurve) + +[![PyPI - Version](https://img.shields.io/pypi/v/coincurve.svg?logo=pypi&label=PyPI&logoColor=gold)](https://pypi.org/project/coincurve/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/coincurve.svg?color=blue&label=Downloads&logo=pypi&logoColor=gold)](https://pypi.org/project/coincurve/) +[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/coincurve.svg?logo=python&label=Python&logoColor=gold)](https://pypi.org/project/coincurve/) + +[![Code style - black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) +[![License - MIT/Apache-2.0](https://img.shields.io/badge/license-MIT%2FApache--2.0-9400d3.svg)](https://spdx.org/licenses/) +[![GitHub Sponsors](https://img.shields.io/github/sponsors/ofek?logo=GitHub%20Sponsors&style=social)](https://github.com/sponsors/ofek) + +----- + +This library provides well-tested Python bindings for [libsecp256k1][], the heavily optimized +C library used by [Bitcoin Core][] for operations on the elliptic curve [secp256k1][]. + +## Features + +- Fastest available implementation (more than 10x faster than OpenSSL) +- Clean, easy to use API +- Frequent updates from the development version of [libsecp256k1][] +- Linux, macOS, and Windows all have binary packages for multiple architectures +- Linux & macOS use GMP for faster computation +- Deterministic signatures as specified by [RFC 6979][] +- Non-malleable signatures (lower-S form) by default +- Secure, non-malleable [ECDH][] implementation + +## License + +`coincurve` is distributed under the terms of any of the following licenses: + +- [MIT](https://spdx.org/licenses/MIT.html) +- [Apache-2.0](https://spdx.org/licenses/Apache-2.0.html) + +## Navigation + +Desktop readers can use keyboard shortcuts to navigate. + +| Keys | Action | +| --- | --- | +| | Navigate to the "previous" page | +| | Navigate to the "next" page | +| | Display the search modal | diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c4f040e --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,110 @@ +site_name: coincurve +site_description: Cross-platform Python bindings for libsecp256k1 +site_author: Ofek Lev +site_url: https://ofek.dev/coincurve/ +repo_name: ofek/coincurve +repo_url: https://github.com/ofek/coincurve +edit_uri: blob/master/docs +copyright: 'Copyright © Ofek Lev 2017-present' + +docs_dir: docs +site_dir: site +theme: + name: material + language: en + features: + - navigation.sections + # - navigation.instant + palette: + scheme: slate + primary: amber + accent: amber + font: + text: Roboto + code: Roboto Mono + icon: + logo: material/book-open-page-variant + repo: fontawesome/brands/github-alt + favicon: assets/images/favicon.ico + +nav: + - About: index.md + - API Reference: api.md + +plugins: + # Built-in + - search: + # Extra + - minify: + minify_html: true + - git-revision-date-localized: + type: date + - mkdocstrings: + default_handler: python + handlers: + python: + rendering: + show_if_no_docstring: true + show_root_heading: true + show_source: true + +markdown_extensions: + # Built-in + - markdown.extensions.abbr: + - markdown.extensions.admonition: + - markdown.extensions.footnotes: + - markdown.extensions.tables: + - markdown.extensions.toc: + permalink: true + toc_depth: "2-6" + # Extra + - mkpatcher: + location: docs/.scripts + - pymdownx.arithmatex: + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret: + - pymdownx.critic: + - pymdownx.details: + - pymdownx.emoji: + # https://github.com/twitter/twemoji + # https://raw.githubusercontent.com/facelessuser/pymdown-extensions/master/pymdownx/twemoji_db.py + emoji_index: !!python/name:materialx.emoji.twemoji + emoji_generator: !!python/name:materialx.emoji.to_svg + - pymdownx.highlight: + guess_lang: false + linenums_style: pymdownx-inline + use_pygments: true + - pymdownx.inlinehilite: + - pymdownx.keys: + - pymdownx.magiclink: + repo_url_shortener: true + repo_url_shorthand: true + social_url_shorthand: true + provider: github + user: ofek + repo: coincurve + - pymdownx.mark: + - pymdownx.progressbar: + - pymdownx.smartsymbols: + - pymdownx.snippets: + base_path: docs/.snippets + - pymdownx.superfences: + - pymdownx.tabbed: + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde: + +extra: + social: + - icon: fontawesome/brands/github-alt + link: https://github.com/ofek + - icon: fontawesome/solid/blog + link: https://ofek.dev/words/ + - icon: fontawesome/brands/twitter + link: https://twitter.com/Ofekmeister + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/in/ofeklev/ +extra_css: + - assets/css/custom.css + - https://cdn.jsdelivr.net/gh/tonsky/FiraCode@4/distr/fira_code.css diff --git a/tox.ini b/tox.ini index e094bb8..6899503 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ envlist = lint fmt typing + docs [testenv] passenv = * @@ -59,3 +60,36 @@ deps = mypy==0.790 commands = mypy coincurve + +[testenv:docs] +usedevelop = true +setenv = + ; Pretty __repr__ for defaults of complex types + COINCURVE_BUILDING_DOCS=true + ; Use a set timestamp for reproducible builds. + ; See https://reproducible-builds.org/specs/source-date-epoch/ + SOURCE_DATE_EPOCH=1580601600 +deps = + mkdocs~=1.1.2 + ; theme + mkdocs-material~=6.2.5 + ; plugins + mkdocs-minify-plugin~=0.4.0 + mkdocs-git-revision-date-localized-plugin~=0.8 + mkdocstrings~=0.14.0 + ; Extensions + pymdown-extensions~=8.1 + mkdocs-material-extensions~=1.0.1 + mkpatcher~=1.0.2 + ; Necessary for syntax highlighting in code blocks + Pygments~=2.7.4 +commands = + python -m mkdocs {posargs} + +[testenv:docs-ci] +setenv = {[testenv:docs]setenv} +deps = {[testenv:docs]deps} +commands = + python -c "import shutil; shutil.move('coincurve', '_coincurve')" + {[testenv:docs]commands} + python -c "import shutil; shutil.move('_coincurve', 'coincurve')"