Skip to main content
Key management is critical to Ubu-Block’s security model. This page explains how keys are generated, stored, and used throughout the system.

Key Types

Ubu-Block uses asymmetric cryptography with two types of keys:

Private Keys (SigningKey)

  • Used to create digital signatures
  • Must be kept secret and secure
  • Stored in the private_db database
  • Never transmitted over the network
  • Required to create new blocks

Public Keys (VerifyingKey)

  • Used to verify digital signatures
  • Can be shared publicly
  • Stored in the chain_db database
  • Distributed to all nodes
  • Used to validate blocks
Your private key is like a password that cannot be reset. If you lose it, you cannot create new blocks. If someone steals it, they can create blocks as you.

Key Generation

Keys are generated using cryptographically secure random numbers:
use p256::ecdsa::SigningKey;
use p256::elliptic_curve::rand_core::OsRng;

pub fn get_private_key() -> SigningKey {
    SigningKey::random(&mut OsRng)
}
The corresponding public key is derived mathematically:
pub fn get_public_key(key: &SigningKey) -> VerifyingKey {
    *key.verifying_key()
}
The public key is derived from the private key using elliptic curve mathematics. This is a one-way operation - you cannot derive the private key from the public key.

Key Storage

Ubu-Block uses two separate SQLite databases for key storage:

Private Key Database (private_db)

Stores private keys that belong to this node:
pub async fn add_private_key(
    &self,
    priv_key: &Vec<u8>,
    pub_key_hash: &str,
) -> Result<i64, sqlx::Error> {
    let sql = "INSERT INTO privkeys(pubkey_hash, privkey, time_added) VALUES (?, ?, ?)";
    let res = sqlx::query(sql)
        .bind(pub_key_hash)
        .bind(hex::encode(priv_key))
        .bind(Utc::now().timestamp())
        .execute(&mut *pool)
        .await?
        .last_insert_rowid();
    Ok(res)
}
See crates/database/src/lib.rs:115-130 for the full implementation. Private key storage includes:
  • pubkey_hash - SHA3-256 hash of the corresponding public key
  • privkey - Hex-encoded private key bytes
  • time_added - Unix timestamp when the key was added
The private_db database contains sensitive cryptographic material. Protect it with appropriate file system permissions and backup securely.

Public Key Database (chain_db)

Stores all public keys in the network:
pub async fn add_public_key(
    &self,
    pub_key: &[u8],
    creator: &str,
    pubkey_hash: &str,
    block_height: i32,
) -> Result<i64, sqlx::Error> {
    let sql = "INSERT INTO pubkeys(pubkey_hash, creator, pubkey, state, time_added, block_height) VALUES (?, ?, ?, ?, ?, ?)";
    let res = sqlx::query(sql)
        .bind(pubkey_hash)
        .bind(creator)
        .bind(hex::encode(pub_key))
        .bind("A") // Active state
        .bind(Utc::now().timestamp())
        .bind(block_height)
        .execute(&mut *pool)
        .await?
        .last_insert_rowid();
    Ok(res)
}
See crates/database/src/lib.rs:93-113 for the full implementation. Public key storage includes:
  • pubkey_hash - SHA3-256 hash of the public key
  • creator - Human-readable identifier for the key owner
  • pubkey - Hex-encoded public key bytes
  • state - Key state (“A” for active, revoked keys have different states)
  • time_added - Unix timestamp when added
  • block_height - Block number when the key was added
  • time_revoked - Optional timestamp if the key was revoked

Key Retrieval

Getting Your Private Key

When creating a new block, the node retrieves its private key:
pub async fn get_private_key(&self) -> Result<(SigningKey, VerifyingKey, PubKey), sqlx::Error> {
    let (private_key, public_key_hash) = self.get_private_key_from_db().await?;
    let pub_key = self.get_public_key(&public_key_hash).await?;
    let private_key = SigningKey::from_slice(&hex::decode(&private_key).unwrap()).unwrap();
    let public_key = deserialize(&pub_key.bytes).unwrap();
    Ok((private_key, public_key, pub_key))
}
See crates/database/src/lib.rs:147-154 for the full implementation. This returns a tuple containing:
  1. SigningKey - Used to sign new blocks
  2. VerifyingKey - Used for signature verification
  3. PubKey - Metadata about the key

Getting a Public Key by Hash

When verifying a block, the node looks up the creator’s public key:
pub async fn get_public_key(&self, hash: &str) -> Result<PubKey, sqlx::Error> {
    let sql = "SELECT pubkey_hash, pubkey, state, time_added, COALESCE(time_revoked, -1), block_height, creator FROM pubkeys WHERE pubkey_hash = ?1";
    // ... query execution ...
    Ok(PubKey {
        hash: res.0,
        bytes: hex::decode(res.1).unwrap(),
        state: res.2,
        time_added: res.3,
        is_revoked,
        time_revoked,
        add_block_height: res.5 as usize,
        creator: res.6,
    })
}
See crates/database/src/lib.rs:156-183 for the full implementation.

Public Key Structure

The PubKey struct contains:
pub struct PubKey {
    pub hash: String,           // SHA3-256 hash identifier
    pub creator: String,        // Human-readable name
    pub bytes: Vec<u8>,         // Raw public key bytes
    pub state: String,          // Active/revoked status
    pub time_added: DateTime<Utc>,
    pub is_revoked: bool,
    pub time_revoked: Option<DateTime<Utc>>,
    pub add_block_height: usize, // When added to blockchain
}

Key Hashing

Public keys are hashed to create shorter identifiers:
let sigkey_hash = sha256_digest(&signer.1);
This hash is used throughout the system:
  • Stored in block headers as signature_pub_key_hash
  • Used to look up the full public key during verification
  • Provides a consistent-length identifier
Hashing public keys provides several benefits:
  • Shorter identifiers (64 characters vs. variable length)
  • Consistent format across the system
  • Additional layer of abstraction

Key Lifecycle

1. Generation

When initializing a node:
cargo run init --source setup_constituencies.sql
This generates a new key pair and stores it in both databases.

2. Active Use

When creating blocks:
let signer = database.get_private_key().await?;
let block = Block::new(&signer, &prev_hash, results, height, merkle_root);
The private key signs the block, and the public key hash is included in the block header.

3. Verification

When validating blocks:
let pub_key = self.get_public_key(&block.signature_pub_key_hash).await?;
let verifier: VerifyingKey = deserialize(&pub_key.bytes).unwrap();
verifier.verify(calculated_hash.as_bytes(), &signature).unwrap();
The public key is retrieved and used to verify the signature.

4. Revocation

Keys can be revoked (though the revocation mechanism is not yet fully implemented):
pub time_revoked: Option<DateTime<Utc>>,
pub is_revoked: bool,
Revoked keys should not be used to create new blocks but can still verify old blocks.

Multi-Key Support

A node can manage multiple key pairs:
pub async fn get_my_public_key_hashes(&self) -> Result<Vec<String>, sqlx::Error> {
    let sql = "SELECT pubkey_hash FROM privkeys";
    let res: Vec<(String,)> = sqlx::query_as(sql).fetch_all(&mut *pool).await?;
    let hashes = res.into_iter().map(|r| r.0).collect();
    Ok(hashes)
}
See crates/database/src/lib.rs:132-138 for the full implementation. This allows:
  • Key rotation for security
  • Different keys for different purposes
  • Multiple authorized signers

Security Best Practices

Private Key Protection

Critical Security Measures:
  • Set restrictive file permissions on private.db (e.g., chmod 600)
  • Never commit private keys to version control
  • Use encrypted storage for backups
  • Consider hardware security modules (HSM) for production
  • Never transmit private keys over the network

Backup Strategy

  1. Regular Backups: Back up private.db to secure, encrypted storage
  2. Multiple Locations: Store backups in geographically distributed locations
  3. Access Control: Limit who can access backup files
  4. Test Restoration: Periodically verify you can restore from backups

Key Rotation

While not required, periodic key rotation improves security:
  • Generate a new key pair
  • Add the new public key to the blockchain
  • Start using the new key for future blocks
  • Keep the old key for historical verification

Database Schema

Private Keys Table

CREATE TABLE privkeys (
    pubkey_hash TEXT PRIMARY KEY,
    privkey TEXT NOT NULL,
    time_added INTEGER NOT NULL
);

Public Keys Table

CREATE TABLE pubkeys (
    pubkey_hash TEXT PRIMARY KEY,
    creator TEXT NOT NULL,
    pubkey TEXT NOT NULL,
    state TEXT NOT NULL,
    time_added INTEGER NOT NULL,
    time_revoked INTEGER,
    block_height INTEGER NOT NULL
);

Integration with Block Creation

When creating a new block, the system:
  1. Retrieves the private key from private_db
  2. Hashes the corresponding public key
  3. Includes the hash in the block header
  4. Signs the block hash with the private key
  5. Signs the previous block hash
  6. Stores both signatures in the new block
See crates/types/src/lib.rs:74-111 for the block creation implementation.

Troubleshooting

Key Not Found

If you get “key not found” errors:
# Check what keys you have
sqlite3 data/private.db "SELECT * FROM privkeys;"
sqlite3 data/blockchain.db "SELECT pubkey_hash, creator FROM pubkeys;"

Invalid Signature

If signature verification fails:
  1. Verify the public key hash matches
  2. Check the key hasn’t been corrupted
  3. Ensure you’re using the correct key for verification
  4. Validate the key was active when the block was created

Database Corruption

If key databases are corrupted:
  1. Restore from backup immediately
  2. Validate blockchain integrity after restoration
  3. Re-sync with other nodes if necessary
If you lose your private key without a backup, you cannot create new blocks with that identity. Always maintain secure backups.

Build docs developers (and LLMs) love