diff --git a/.github/workflows/build_python.yml b/.github/workflows/build_python.yml new file mode 100644 index 0000000..7090768 --- /dev/null +++ b/.github/workflows/build_python.yml @@ -0,0 +1,30 @@ +name: Rust-Python + +on: + push: + branches: [ "main", "develop", "staging" ] + pull_request: + branches: [ "main", "develop", "staging" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - name: Set up Rust + run: sudo apt-get install cargo && rustup override set 1.81 + - name: Set up Python + run: sudo apt-get install python3 python3-dev + - uses: actions/checkout@v4 + - name: Set up a Python virtual environment (needed for maturin) + run: python3 -m venv ./venv + - name: Install Python deps + run: ./venv/bin/pip3 install -r ./lib-py/requirements.txt + - name: Build the Rust library, install it as a Python module + run: cd lib-py && VIRTUAL_ENV='../venv' ../venv/bin/maturin develop && cd .. + - name: Run the Python sample app + run: cd samples/python && ../../venv/bin/python main.py && cd ../.. diff --git a/.github/workflows/build_test.yml b/.github/workflows/build_test.yml index 2c96b9d..ba7611e 100644 --- a/.github/workflows/build_test.yml +++ b/.github/workflows/build_test.yml @@ -20,8 +20,6 @@ jobs: - uses: actions/checkout@v4 - name: Build the library run: cargo build - - name: Build with pyo3 - run: cargo build --features with-pyo3 - name: Run unit tests run: cargo test - name: Build and run sample/rust diff --git a/Cargo.toml b/Cargo.toml index 231c067..1e81773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dlccryptlib" -version = "0.3.2" +version = "0.9.0" edition = "2021" description = "Library for working with DLC's with adaptor signatures (Discrete Log Contracts), by Cadena Bitcoin" license = "MIT" @@ -13,12 +13,9 @@ crate-type = ["cdylib", "rlib"] [dependencies] bip39 = "2.1.0" bitcoin = "0.32.4" -pyo3 = { version = "0.23.1", optional = true } secp256k1-sys = "0.10.1" secp256k1-zkp = "0.11.0" [features] default = ["std"] std = ["bitcoin/std", "secp256k1-zkp/rand-std"] -# To compile with Python interfacing methods -with-pyo3 = ["pyo3"] diff --git a/README.md b/README.md index 703c66c..158e905 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,20 @@ To build and test in Rust: cargo build && cargo test ``` +Publish the library: + +``` +cargo publish +``` + +To build the Python-wrapper library: + +``` +cd lib-py +./venv/bin/pip install -r requirements.txt +VIRTUAL_ENV="./venv" ./venv/bin/maturin develop +``` + ## Samples ``` diff --git a/lib-py/Cargo.toml b/lib-py/Cargo.toml new file mode 100644 index 0000000..a92ac30 --- /dev/null +++ b/lib-py/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "dlccryptlib-py" +version = "0.9.0" +edition = "2021" +description = "Python-wrapped library for working with DLC's with adaptor signatures (Discrete Log Contracts), by Cadena Bitcoin" +license = "MIT" + +[lib] +name = "dlccryptlib_py" + +[dependencies] +#dlccryptlib = "0.9.0" +dlccryptlib = { path = "../" } +pyo3 = { version = "0.23.1" } diff --git a/lib-py/requirements.txt b/lib-py/requirements.txt new file mode 100644 index 0000000..b8be672 --- /dev/null +++ b/lib-py/requirements.txt @@ -0,0 +1,2 @@ +# for Rust +maturin diff --git a/lib-py/src/lib.rs b/lib-py/src/lib.rs new file mode 100644 index 0000000..9062261 --- /dev/null +++ b/lib-py/src/lib.rs @@ -0,0 +1,192 @@ +use dlccryptlib; + +use pyo3::prelude::*; +use pyo3::exceptions::PyException; +use pyo3::wrap_pyfunction; + + +// ##### Facade functions for easy Python invocations (pyo3/maturin) + +/// Initialize the library, load secret from encrypted file. Return the XPUB. +#[pyfunction] +pub fn init(path_for_secret_file: String, encryption_password: String) -> PyResult { + dlccryptlib::init_intern(&path_for_secret_file, &encryption_password, false) + .map_err(|e| PyErr::new::(e)) +} + +#[pyfunction] +pub fn reinit_for_testing( + path_for_secret_file: String, + encryption_password: String, +) -> PyResult { + dlccryptlib::init_intern(&path_for_secret_file, &encryption_password, true) + .map_err(|e| PyErr::new::(e)) +} + +/// network: "bitcoin", or "signet". +// #[cfg(test)] +#[pyfunction] +pub fn init_with_entropy(entropy: String, network: String) -> PyResult { + dlccryptlib::init_with_entropy_intern(&entropy, &network).map_err(|e| PyErr::new::(e)) +} + +/// Return the XPUB +#[pyfunction] +pub fn get_xpub() -> PyResult { + dlccryptlib::get_xpub_intern().map_err(|e| PyErr::new::(e)) +} + +/// Return a child public key (specified by its index). +#[pyfunction] +pub fn get_public_key(index: u32) -> PyResult { + dlccryptlib::get_public_key_intern(index).map_err(|e| PyErr::new::(e)) +} + +/// Return a child address (specified by index). +#[pyfunction] +pub fn get_address(index: u32) -> PyResult { + dlccryptlib::get_address_intern(index).map_err(|e| PyErr::new::(e)) +} + +/// Verify a child public key. +#[pyfunction] +pub fn verify_public_key(index: u32, pubkey: String) -> PyResult { + dlccryptlib::verify_public_key_intern(index, &pubkey).map_err(|e| PyErr::new::(e)) +} + +/// Sign a hash with a child private key (specified by index). +#[pyfunction] +pub fn sign_hash_ecdsa(hash: String, signer_index: u32, signer_pubkey: String) -> PyResult { + dlccryptlib::sign_hash_ecdsa_intern(&hash, signer_index, &signer_pubkey) + .map_err(|e| PyErr::new::(e)) +} + +/// Create a nonce value deterministically +#[pyfunction] +pub fn create_deterministic_nonce( + event_id: String, + nonce_index: u32, +) -> PyResult<(String, String)> { + dlccryptlib::create_deterministic_nonce_intern(&event_id, nonce_index) + .map_err(|e| PyErr::new::(e)) +} + +/// Sign a message using Schnorr, with a nonce, using a child key +#[pyfunction] +pub fn sign_schnorr_with_nonce(msg: String, nonce_sec_hex: String, index: u32) -> PyResult { + dlccryptlib::sign_schnorr_with_nonce_intern(&msg, &nonce_sec_hex, index) + .map_err(|e| PyErr::new::(e)) +} + +/// Combine a number of public keys into one +#[pyfunction] +pub fn combine_pubkeys(keys_hex: String) -> PyResult { + dlccryptlib::combine_pubkeys_intern(&keys_hex).map_err(|e| PyErr::new::(e)) +} + +/// Combine a number of secret keys into one. +/// Warning: Handle secret keys with caution! +#[pyfunction] +pub fn combine_seckeys(keys_hex: String) -> PyResult { + dlccryptlib::combine_seckeys_intern(&keys_hex).map_err(|e| PyErr::new::(e)) +} + +/// Create adaptor signatures for a number of CETs +#[pyfunction] +pub fn create_cet_adaptor_sigs( + num_digits: u8, + num_cets: u64, + digit_string_template: String, + oracle_pubkey: String, + signing_key_index: u32, + signing_pubkey: String, + nonces: String, + interval_wildcards: String, + sighashes: String, +) -> PyResult { + dlccryptlib::create_cet_adaptor_sigs_intern( + num_digits, + num_cets, + &digit_string_template, + &oracle_pubkey, + signing_key_index, + &signing_pubkey, + &nonces, + &interval_wildcards, + &sighashes, + ) + .map_err(|e| PyErr::new::(e)) +} + +/// Verify adaptor signatures for a number of CETs +#[pyfunction] +pub fn verify_cet_adaptor_sigs( + num_digits: u8, + num_cets: u64, + digit_string_template: String, + oracle_pubkey: String, + signing_pubkey: String, + nonces: String, + interval_wildcards: String, + sighashes: String, + signatures: String, +) -> PyResult { + dlccryptlib::verify_cet_adaptor_sigs_intern( + num_digits, + num_cets, + &digit_string_template, + &oracle_pubkey, + &signing_pubkey, + &nonces, + &interval_wildcards, + &sighashes, + &signatures, + ) + .map_err(|e| PyErr::new::(e)) +} + +/// Perform final signing of a CET +#[pyfunction] +pub fn create_final_cet_sigs( + signing_key_index: u32, + signing_pubkey: String, + other_pubkey: String, + num_digits: u8, + oracle_signatures: String, + cet_value_wildcard: String, + cet_sighash: String, + other_adaptor_signature: String, +) -> PyResult { + dlccryptlib::create_final_cet_sigs_intern( + signing_key_index, + &signing_pubkey, + &other_pubkey, + num_digits, + &oracle_signatures, + &cet_value_wildcard, + &cet_sighash, + &other_adaptor_signature, + ) + .map_err(|e| PyErr::new::(e)) +} + +#[pymodule] +fn dlccryptlib_py(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(init, m)?)?; + m.add_function(wrap_pyfunction!(reinit_for_testing, m)?)?; + m.add_function(wrap_pyfunction!(init_with_entropy, m)?)?; + m.add_function(wrap_pyfunction!(get_xpub, m)?)?; + m.add_function(wrap_pyfunction!(get_public_key, m)?)?; + m.add_function(wrap_pyfunction!(get_address, m)?)?; + m.add_function(wrap_pyfunction!(verify_public_key, m)?)?; + m.add_function(wrap_pyfunction!(sign_hash_ecdsa, m)?)?; + m.add_function(wrap_pyfunction!(create_deterministic_nonce, m)?)?; + m.add_function(wrap_pyfunction!(sign_schnorr_with_nonce, m)?)?; + m.add_function(wrap_pyfunction!(combine_pubkeys, m)?)?; + m.add_function(wrap_pyfunction!(combine_seckeys, m)?)?; + m.add_function(wrap_pyfunction!(create_cet_adaptor_sigs, m)?)?; + m.add_function(wrap_pyfunction!(verify_cet_adaptor_sigs, m)?)?; + m.add_function(wrap_pyfunction!(create_final_cet_sigs, m)?)?; + Ok(()) +} + diff --git a/samples/python/main.py b/samples/python/main.py new file mode 100644 index 0000000..df8fcb9 --- /dev/null +++ b/samples/python/main.py @@ -0,0 +1,21 @@ +import dlccryptlib_py + +def sample(): + print("Dlccryptlib sample app, Python") + + entropy_hex = "99d33a674ce99d33a674ce99d33a674c" # oil x 12 + network = "signet"; + + xpub = dlccryptlib_py.init_with_entropy(entropy_hex, network) + print(f"Library initialized, xpub {xpub}") + + pubkey0 = dlccryptlib_py.get_public_key(0) + print(f"pubkey 0: {pubkey0}") + + hash = "0001020300000000000000000000000000000000000000000000000000010203" + sig = dlccryptlib_py.sign_hash_ecdsa(hash, 0, pubkey0) + print(f"signature: {sig}") + +if __name__ == "__main__": + sample() + diff --git a/samples/python/requirements.txt b/samples/python/requirements.txt new file mode 100644 index 0000000..0956260 --- /dev/null +++ b/samples/python/requirements.txt @@ -0,0 +1,2 @@ +dlccryptlib-py + diff --git a/samples/rust/Cargo.toml b/samples/rust/Cargo.toml index f1d86f4..57767ab 100644 --- a/samples/rust/Cargo.toml +++ b/samples/rust/Cargo.toml @@ -4,5 +4,5 @@ version = "0.1.0" edition = "2021" [dependencies] -#dlccryptlib = "0.3.2" +#dlccryptlib = "0.9.0" dlccryptlib = { path = "../../" } diff --git a/src/lib.rs b/src/lib.rs index c86c533..7bfc2f8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -558,213 +558,3 @@ fn error_as_cstr_prefix(error: String) -> *mut c_char { .into_raw() } -// Conditional compilation to exclude PyO3-related code for Android -#[cfg(feature = "with-pyo3")] -use pyo3::prelude::*; -#[cfg(feature = "with-pyo3")] -use pyo3::exceptions::PyException; -#[cfg(feature = "with-pyo3")] -use pyo3::wrap_pyfunction; - - -// ##### Facade functions for easy Python invocations (pyo3/maturin) - -/// Initialize the library, load secret from encrypted file. Return the XPUB. -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn init(path_for_secret_file: String, encryption_password: String) -> PyResult { - init_intern(&path_for_secret_file, &encryption_password, false) - .map_err(|e| PyErr::new::(e)) -} - -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn reinit_for_testing( - path_for_secret_file: String, - encryption_password: String, -) -> PyResult { - init_intern(&path_for_secret_file, &encryption_password, true) - .map_err(|e| PyErr::new::(e)) -} - -/// network: "bitcoin", or "signet". -// #[cfg(test)] -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn init_with_entropy(entropy: String, network: String) -> PyResult { - init_with_entropy_intern(&entropy, &network).map_err(|e| PyErr::new::(e)) -} - -/// Return the XPUB -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn get_xpub() -> PyResult { - get_xpub_intern().map_err(|e| PyErr::new::(e)) -} - -/// Return a child public key (specified by its index). -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn get_public_key(index: u32) -> PyResult { - get_public_key_intern(index).map_err(|e| PyErr::new::(e)) -} - -/// Return a child address (specified by index). -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn get_address(index: u32) -> PyResult { - get_address_intern(index).map_err(|e| PyErr::new::(e)) -} - -/// Verify a child public key. -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn verify_public_key(index: u32, pubkey: String) -> PyResult { - verify_public_key_intern(index, &pubkey).map_err(|e| PyErr::new::(e)) -} - -/// Sign a hash with a child private key (specified by index). -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn sign_hash_ecdsa(hash: String, signer_index: u32, signer_pubkey: String) -> PyResult { - sign_hash_ecdsa_intern(&hash, signer_index, &signer_pubkey) - .map_err(|e| PyErr::new::(e)) -} - -/// Create a nonce value deterministically -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn create_deterministic_nonce( - event_id: String, - nonce_index: u32, -) -> PyResult<(String, String)> { - create_deterministic_nonce_intern(&event_id, nonce_index) - .map_err(|e| PyErr::new::(e)) -} - -/// Sign a message using Schnorr, with a nonce, using a child key -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn sign_schnorr_with_nonce(msg: String, nonce_sec_hex: String, index: u32) -> PyResult { - sign_schnorr_with_nonce_intern(&msg, &nonce_sec_hex, index) - .map_err(|e| PyErr::new::(e)) -} - -/// Combine a number of public keys into one -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn combine_pubkeys(keys_hex: String) -> PyResult { - combine_pubkeys_intern(&keys_hex).map_err(|e| PyErr::new::(e)) -} - -/// Combine a number of secret keys into one. -/// Warning: Handle secret keys with caution! -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn combine_seckeys(keys_hex: String) -> PyResult { - combine_seckeys_intern(&keys_hex).map_err(|e| PyErr::new::(e)) -} - -/// Create adaptor signatures for a number of CETs -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn create_cet_adaptor_sigs( - num_digits: u8, - num_cets: u64, - digit_string_template: String, - oracle_pubkey: String, - signing_key_index: u32, - signing_pubkey: String, - nonces: String, - interval_wildcards: String, - sighashes: String, -) -> PyResult { - create_cet_adaptor_sigs_intern( - num_digits, - num_cets, - &digit_string_template, - &oracle_pubkey, - signing_key_index, - &signing_pubkey, - &nonces, - &interval_wildcards, - &sighashes, - ) - .map_err(|e| PyErr::new::(e)) -} - -/// Verify adaptor signatures for a number of CETs -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn verify_cet_adaptor_sigs( - num_digits: u8, - num_cets: u64, - digit_string_template: String, - oracle_pubkey: String, - signing_pubkey: String, - nonces: String, - interval_wildcards: String, - sighashes: String, - signatures: String, -) -> PyResult { - verify_cet_adaptor_sigs_intern( - num_digits, - num_cets, - &digit_string_template, - &oracle_pubkey, - &signing_pubkey, - &nonces, - &interval_wildcards, - &sighashes, - &signatures, - ) - .map_err(|e| PyErr::new::(e)) -} - -/// Perform final signing of a CET -#[cfg(feature = "with-pyo3")] -#[pyfunction] -pub fn create_final_cet_sigs( - signing_key_index: u32, - signing_pubkey: String, - other_pubkey: String, - num_digits: u8, - oracle_signatures: String, - cet_value_wildcard: String, - cet_sighash: String, - other_adaptor_signature: String, -) -> PyResult { - create_final_cet_sigs_intern( - signing_key_index, - &signing_pubkey, - &other_pubkey, - num_digits, - &oracle_signatures, - &cet_value_wildcard, - &cet_sighash, - &other_adaptor_signature, - ) - .map_err(|e| PyErr::new::(e)) -} - -#[cfg(feature = "with-pyo3")] -#[pymodule] -fn dlcplazacryptlib(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(init, m)?)?; - m.add_function(wrap_pyfunction!(reinit_for_testing, m)?)?; - m.add_function(wrap_pyfunction!(init_with_entropy, m)?)?; - m.add_function(wrap_pyfunction!(get_xpub, m)?)?; - m.add_function(wrap_pyfunction!(get_public_key, m)?)?; - m.add_function(wrap_pyfunction!(get_address, m)?)?; - m.add_function(wrap_pyfunction!(verify_public_key, m)?)?; - m.add_function(wrap_pyfunction!(sign_hash_ecdsa, m)?)?; - m.add_function(wrap_pyfunction!(create_deterministic_nonce, m)?)?; - m.add_function(wrap_pyfunction!(sign_schnorr_with_nonce, m)?)?; - m.add_function(wrap_pyfunction!(combine_pubkeys, m)?)?; - m.add_function(wrap_pyfunction!(combine_seckeys, m)?)?; - m.add_function(wrap_pyfunction!(create_cet_adaptor_sigs, m)?)?; - m.add_function(wrap_pyfunction!(verify_cet_adaptor_sigs, m)?)?; - m.add_function(wrap_pyfunction!(create_final_cet_sigs, m)?)?; - Ok(()) -} -