Browse Source

add docs (#90)

* add docs

* Update mkdocs.yml

* Update tox.ini

* .

* Update tox.ini

* .

* Create FUNDING.yml

* Update index.md
master
Ofek Lev 3 years ago
committed by GitHub
parent
commit
64e25d98ca
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .github/FUNDING.yml
  2. 62
      .github/workflows/docs.yml
  3. 2
      .gitignore
  4. 9
      coincurve/context.py
  5. 2
      coincurve/ecdsa.py
  6. 208
      coincurve/keys.py
  7. 5
      coincurve/types.py
  8. 41
      coincurve/utils.py
  9. 3
      docs/.scripts/49_global_refs.py
  10. 2
      docs/.snippets/abbrs.txt
  11. 5
      docs/.snippets/links.txt
  12. 4
      docs/.snippets/refs.txt
  13. 50
      docs/api.md
  14. 61
      docs/assets/css/custom.css
  15. BIN
      docs/assets/images/favicon.ico
  16. 46
      docs/index.md
  17. 110
      mkdocs.yml
  18. 34
      tox.ini

5
.github/FUNDING.yml

@ -0,0 +1,5 @@
github:
- ofek
custom:
- https://ofek.dev/donate/
- https://paypal.me/ofeklev

62
.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

2
.gitignore

@ -9,7 +9,7 @@
/coincurve.egg-info
/build
/dist
/docs/build
/site
/wheelhouse
/pip-wheel-metadata

9
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')

2
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:

208
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(

5
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]

41
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))

3
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"', ''))

2
docs/.snippets/abbrs.txt

@ -0,0 +1,2 @@
*[ECDH]: Elliptic-curve Diffie–Hellman
*[PyPI]: Python Package Index

5
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

4
docs/.snippets/refs.txt

@ -0,0 +1,4 @@
--8<--
links.txt
abbrs.txt
--8<--

50
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

61
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;
}

BIN
docs/assets/images/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

46
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 |
| --- | --- |
| <ul><li><kbd>,</kbd> (comma)</li><li><kbd>p</kbd></li></ul> | Navigate to the "previous" page |
| <ul><li><kbd>.</kbd> (period)</li><li><kbd>n</kbd></li></ul> | Navigate to the "next" page |
| <ul><li><kbd>/</kbd></li><li><kbd>s</kbd></li></ul> | Display the search modal |

110
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 &copy; 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

34
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')"

Loading…
Cancel
Save