Skip to content
QuantovaPublic

About

Quantova's client core for Python. Post quantum signing and Q1 addresses, generated over the Rust core.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

35 stars

Watchers

7 watching

Forks

Latest commit

 

History

175 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

QCore.py

The Quantova post quantum client core for Python. It is the Rust core QCore.rs built as a native extension with pyo3, so the key derivation, the post quantum signing, and every RPC request body are the core's and are never rewritten in Python. This package does the HTTP and reads the documented response fields, and the core does everything that has to be exactly right. A second implementation of the signing would be a second chance to be wrong with a user's money, so there is only ever the one.

One core with one binding for each language. QCore.rs is the Rust core. QCore.js is the JavaScript binding over it. QCore.py is this Python binding over it. It imports as quantova_qcore, a name distinct from the unrelated qcore project, and the distribution is quantova-qcore. It is not published on PyPI, and the project named qcore on PyPI is an unrelated package, so never install it with pip install qcore. Earlier releases imported as qcore, and that name is gone so it can never be confused with the unrelated package. Build it from this repository with maturin as described under Building.

What this library is for

Any Python program that holds a Quantova account, reads the chain, and sends signed transactions. A backend service, an indexer, an explorer, a faucet, a validator tool, or a script. Your code drives the flow it wants and QCore.py derives the key, signs the transaction, and builds the request body, so nothing about the cryptography or the byte layout of a transaction is written in Python.

The post quantum cryptography it handles

Quantova is post quantum from the ground. There is no elliptic curve anywhere in it, no secp256k1, no ECDSA, no Ethereum address, and no Substrate envelope. Every primitive below is Quantova's own from scratch implementation in the Q Crypto library, carrying no third party cryptography dependency, compiled into the native extension that ships with this package.

  1. Key derivation. Your program holds one master seed of thirty two bytes. QCore.py grows a per account seed from it with SHAKE256 over the master seed, the scheme byte, and the account index, then derives a module lattice key pair from that seed. The node runs the identical derivation, so the account a key signs from is the account that holds the funds.

  2. The signing scheme. The default is the module lattice signature ML-DSA-65 as standardised in FIPS 204, carried on the wire as scheme one. The hash based signature SLH-DSA as standardised in FIPS 205 is scheme two. Signing is deterministic, so one transaction body always signs to one exact run of bytes, which makes a signed transaction reproducible and testable.

  3. The address. A Quantova address is the Bech32m Q1 string that renders the SHA3 256 hash of the scheme byte together with the whole module lattice public key of one thousand nine hundred fifty two bytes. The entire public key is bound into the address. Nothing is truncated to a twenty byte hash and there is no key recovery from a signature, so one address names exactly one post quantum key.

  4. The transaction. A transaction body carries the sender, the nonce, the meter limit, the fee, and the call. A call may also carry a value in Quon and a chain id, so a signature can pay a call and can never be replayed onto another network. The bytes a signature stands over are the SHA3 256 hash of the canonical body, and a fixed Quantova transaction domain tag is bound in as the post quantum signing context, so a transaction signature can never be replayed as another kind of signed message. Every address the body carries is bound in as the raw thirty two byte payload underneath the Q1 string, never as the string itself, so two spellings of the same address always sign to the same bytes. QCore.py assembles the body, signs the digest inside the native extension, and returns the wrapper bytes and the transaction id ready for the gateway.

How it is customised to Quantova and inherits nothing from the industry

Quantova shares no wire, no address, and no unit with any other chain, and QCore.py speaks only Quantova. The address is a Q1 Bech32m string over a full post quantum key, never a hex twenty byte address and never an SS58 string. Money is counted in Quon, the smallest unit, at one million Quon to one QTOV, and it comes back as a decimal string that you convert with int only where a whole number is needed. The wire is the Quantova gateway, an HTTP POST to a named method under a version prefix with a flat JSON body, not Ethereum JSON RPC and not a Substrate WebSocket. The transaction encoding is Quantova's own canonical codec, not RLP and not SCALE. The signatures come from Q Crypto, Quantova's own implementation of the lattice and hash standards written from scratch, not a borrowed library.

Creating a wallet

import quantova_qcore as qcore

seed = qcore.generate_seed()             # thirty two random bytes in a bytearray from the platform source
phrase = qcore.mnemonic_from_seed(seed)  # the only backup as UTF-8 bytes in a bytearray you can wipe

qcore.seed_from_mnemonic(phrase) restores the seed from the twenty five words as a bytearray. It ignores case and extra spacing and folds full width and other compatibility letters through a fixed table before it looks a word up, so a phrase pasted with capitals, non breaking spaces, or full width letters restores the same seed. A standard BIP-39 phrase from another wallet is refused with its own error, since a Quantova phrase carries a SHA3 checksum and cannot be restored from it. It takes the phrase as a str, bytes or bytearray. qcore.mnemonic_from_seed returns a bytearray, so overwrite it with zeros once the words have been shown, and qcore.mnemonic_from_seed_text returns a str for a caller that must display the words. A Python str can never be wiped, so prefer the bytes.

The Client checks every number the gateway reports before it signs. A fee or balance must be a string of the ASCII digits zero to nine, a nonce or head height must be a JSON integer, and the head is bounded by the time since the genesis of the named network, a constant read from the Rust core so all three QCore clients share one value. The Client keeps a signing session in memory. A nonce it signed once is not signed again until you pass that nonce as expected_nonce, and a gateway that rejects a transaction and then reports a later nonce is refused for the same reason. Pass address_book, a list of Q1 addresses you trust, and the Client refuses to sign for a target that shares two or more leading and trailing characters with a known address, eight in all, but is a different address. The Client also reads contract state with storage, events, contract_nonce, contract_scalar, contract_map and asset_balance, and reaches https gateways with certificate verification and TLS 1.2 or later.

Client.call, Client.payable_call and Client.asset_call sign a contract call, a call that moves Quon and a call that moves an issued asset, each bounded by the fee ceiling you pass.

Every function that takes a seed accepts a bytearray or bytes of thirty two bytes and refuses a hex string. Keep the seed in a bytearray and wipe it with seed[:] = bytes(32) when you are done, and never print or log any part of it. The native extension wipes its own copy of a seed before it frees it.

Using it

A Client opened for a plain url with no network signs only for a private chain. It refuses the public testnet and any Q-main-net- chain until you name the network, with Network.testnet() or a mainnet network and acknowledge_mainnet=True. An expected nonce you pass must be the nonce the gateway reports, because the chain admits only the next nonce of an account. A rejected submission, or one whose validity deadline has passed, frees its nonce for the next send.

import quantova_qcore as qcore

client = qcore.Client(qcore.Network.testnet())
seed = qcore.generate_seed()
me = client.address(seed, 0)
to = client.address(seed, 1)

# Reads the fee and the nonce, signs inside the core, and submits. Nothing is
# signed or built in Python. The last argument is the highest fee you will accept,
# and the core refuses to sign a fee the gateway reports above it, so a gateway
# cannot inflate the fee and drain the account.
#
# Choose that ceiling yourself. Passing the gateway's own reported fee as the
# maximum nullifies the cap, because a value can never be above itself, so a
# malicious gateway could report any fee and it would always be accepted. Pick a
# fixed number you are willing to pay (the testnet fee is 500 Quon).
MAX_FEE = 1000  # Quon; the cap you enforce, not whatever the gateway reports
info = client.node_info()

# A fresh account holds nothing, so fund it first. On the public testnet a POST to
# https://rpc-testnet.quantova.org/faucet/api/claim with {"address": me, "amount": 10}
# sends 10 TQTOV, and the payment is itself a transaction so give it a few seconds.
#
# A fresh account funded by a transfer arrives with a balance but no key on the
# chain, so it signs this once to install its public key before its first send.
client.register(seed, 0, MAX_FEE)

signed, outcome = client.transfer(seed, 0, to, 1000, MAX_FEE)
status = client.transaction(signed["tx_id"])

The free signing functions such as sign_transfer take the chain head as their last argument and refuse a validity deadline that is not past the head or lies more than 3600 blocks beyond it. A SigningSession reserves each nonce it hands out until it is signed, and a flow that stops early frees its nonce with release.

verify_finality measures signed stake against the whole roster you pass, which matches the chain when every validator in the roster sits on the committee, as on a network of main validators. Pass exactly the validators of the block's epoch. Each roster entry names the validator id, its stake, its attestation key and its sortition root with the number of slots the root covers, exactly as get_validator_set serves them, and every signer must prove its committee membership against its root.

A new session does not take an order nonce above zero from the gateway alone. Name the expected order nonce, or keep client.save_session() and pass it to client.restore_session(state) after a restart so tracked nonces and held signatures survive. The client refuses to sign for a retired chain name such as Q-test-net-1.

client.confirm_transaction(tx_id, roster) confirms a transaction without trusting the gateway. It checks the finality certificate of the transaction's block against the validator roster you pass and trust, then checks that the transaction is inside that block.

client.trust_roster(roster) gives the client a validator roster you trust. From then on the client checks the head the gateway reports against the finality certificate of that block before it signs, so a gateway cannot move a deadline by lying about the head. A mainnet client refuses to sign until it has a trusted roster.

Sending a payable call

A call to a contract can carry a value in Quon alongside its arguments, and every payable call must name the chain it signs for so the signature can never be replayed onto another network. Read the chain id from the node you mean to reach rather than assuming one. The nonce comes from the account. The validity deadline is the height after which the chain drops the transaction, and a deadline of zero, which would never expire, is refused. A call pays one transfer fee for every started 1210 units of its meter limit, so the raw call signers take the transfer fee the node reports and refuse a fee below vm_call_fee(transfer_fee, meter_limit), a meter limit outside 1210 to 12500000, or call arguments above 128 KiB.

import quantova_qcore as qcore

client = qcore.Client(qcore.Network.testnet())
seed = qcore.generate_seed()
target = qcore.address(seed, 1)
info = client.node_info()
transfer_fee = int(info["fee"]["transfer_quon"])

signed = qcore.sign_payable_call(seed, 0, target, "", nonce=3, meter_limit=21000,
                                  fee=qcore.vm_call_fee(transfer_fee, 21000), value=2500000,
                                  chain_id=qcore.testnet_chain_id(),
                                  valid_until=info["head_height"] + 300, transfer_fee=transfer_fee)

Building

maturin develop

The build needs maturin, which you install into a virtual environment with pip. Use maturin build for a release wheel.

A note on releases

Nothing here is published to a registry without an explicit release. This package handles a user's keys and a published version cannot be taken back.

Ownership and license

QCore.py is built and owned by Quantova Inc and is generated over the Rust core QCore.rs, so the signing lives in one place and is never rewritten in Python. It carries none of the industry stack and inherits nothing from it. It is released under the Apache 2.0 and MIT licenses so any wallet, explorer, or service may build on it, and the copyright stays with Quantova Inc.

About

Quantova's client core for Python. Post quantum signing and Q1 addresses, generated over the Rust core.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

35 stars

Watchers

7 watching

Forks

Releases

Packages

Used by

Contributors

Languages