rwadurian/backend/mpc-system/migrations/001_init_schema.up.sql

323 lines
13 KiB
PL/PgSQL

-- MPC Distributed Signature System Database Schema
-- Version: 001
-- Description: Initial schema creation
-- Enable UUID extension
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
-- ============================================
-- Session Coordinator Schema
-- ============================================
-- MPC Sessions table
CREATE TABLE mpc_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_type VARCHAR(20) NOT NULL, -- 'keygen' or 'sign'
threshold_n INTEGER NOT NULL,
threshold_t INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
message_hash BYTEA, -- For Sign sessions
public_key BYTEA, -- Group public key after Keygen completion
created_by VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL,
completed_at TIMESTAMP,
CONSTRAINT chk_threshold CHECK (threshold_t <= threshold_n AND threshold_t > 0),
CONSTRAINT chk_session_type CHECK (session_type IN ('keygen', 'sign')),
CONSTRAINT chk_status CHECK (status IN ('created', 'in_progress', 'completed', 'failed', 'expired'))
);
-- Indexes for mpc_sessions
CREATE INDEX idx_mpc_sessions_status ON mpc_sessions(status);
CREATE INDEX idx_mpc_sessions_created_at ON mpc_sessions(created_at);
CREATE INDEX idx_mpc_sessions_expires_at ON mpc_sessions(expires_at);
CREATE INDEX idx_mpc_sessions_created_by ON mpc_sessions(created_by);
-- Session Participants table
CREATE TABLE participants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES mpc_sessions(id) ON DELETE CASCADE,
party_id VARCHAR(255) NOT NULL,
party_index INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
device_type VARCHAR(50),
device_id VARCHAR(255),
platform VARCHAR(50),
app_version VARCHAR(50),
public_key BYTEA, -- Party identity public key (for authentication)
joined_at TIMESTAMP NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP,
CONSTRAINT chk_participant_status CHECK (status IN ('invited', 'joined', 'ready', 'completed', 'failed')),
UNIQUE(session_id, party_id),
UNIQUE(session_id, party_index)
);
-- Indexes for participants
CREATE INDEX idx_participants_session_id ON participants(session_id);
CREATE INDEX idx_participants_party_id ON participants(party_id);
CREATE INDEX idx_participants_status ON participants(status);
-- ============================================
-- Message Router Schema
-- ============================================
-- MPC Messages table (for offline message caching)
CREATE TABLE mpc_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID NOT NULL REFERENCES mpc_sessions(id) ON DELETE CASCADE,
from_party VARCHAR(255) NOT NULL,
to_parties TEXT[], -- NULL means broadcast
round_number INTEGER NOT NULL,
message_type VARCHAR(50) NOT NULL,
payload BYTEA NOT NULL, -- Encrypted MPC message
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
delivered_at TIMESTAMP,
CONSTRAINT chk_round_number CHECK (round_number >= 0)
);
-- Indexes for mpc_messages
CREATE INDEX idx_mpc_messages_session_id ON mpc_messages(session_id);
CREATE INDEX idx_mpc_messages_to_parties ON mpc_messages USING GIN(to_parties);
CREATE INDEX idx_mpc_messages_delivered_at ON mpc_messages(delivered_at) WHERE delivered_at IS NULL;
CREATE INDEX idx_mpc_messages_created_at ON mpc_messages(created_at);
CREATE INDEX idx_mpc_messages_round ON mpc_messages(session_id, round_number);
-- ============================================
-- Server Party Service Schema
-- ============================================
-- Party Key Shares table (Server Party's own Share)
CREATE TABLE party_key_shares (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
party_id VARCHAR(255) NOT NULL,
party_index INTEGER NOT NULL,
session_id UUID NOT NULL, -- Keygen session ID
threshold_n INTEGER NOT NULL,
threshold_t INTEGER NOT NULL,
share_data BYTEA NOT NULL, -- Encrypted tss-lib LocalPartySaveData
public_key BYTEA NOT NULL, -- Group public key
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMP,
CONSTRAINT chk_key_share_threshold CHECK (threshold_t <= threshold_n)
);
-- Indexes for party_key_shares
CREATE INDEX idx_party_key_shares_party_id ON party_key_shares(party_id);
CREATE INDEX idx_party_key_shares_session_id ON party_key_shares(session_id);
CREATE INDEX idx_party_key_shares_public_key ON party_key_shares(public_key);
CREATE UNIQUE INDEX idx_party_key_shares_unique ON party_key_shares(party_id, session_id);
-- ============================================
-- Account Service Schema
-- ============================================
-- Accounts table
-- Only username is required, all other fields are optional
CREATE TABLE accounts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(255) UNIQUE NOT NULL,
email VARCHAR(255),
phone VARCHAR(50),
public_key BYTEA, -- MPC group public key (optional, set after keygen)
keygen_session_id UUID, -- Related Keygen session (optional)
threshold_n INTEGER,
threshold_t INTEGER,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
last_login_at TIMESTAMP,
CONSTRAINT chk_account_status CHECK (status IN ('active', 'suspended', 'locked', 'recovering'))
);
-- Indexes for accounts
CREATE INDEX idx_accounts_username ON accounts(username);
CREATE INDEX idx_accounts_email ON accounts(email) WHERE email IS NOT NULL;
CREATE UNIQUE INDEX idx_accounts_email_unique ON accounts(email) WHERE email IS NOT NULL;
CREATE INDEX idx_accounts_public_key ON accounts(public_key);
CREATE INDEX idx_accounts_status ON accounts(status);
-- Account Share Mapping table (records share locations, not share content)
CREATE TABLE account_shares (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
share_type VARCHAR(20) NOT NULL, -- 'user_device', 'server', 'recovery'
party_id VARCHAR(255) NOT NULL,
party_index INTEGER NOT NULL,
device_type VARCHAR(50),
device_id VARCHAR(255),
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMP,
is_active BOOLEAN DEFAULT TRUE,
CONSTRAINT chk_share_type CHECK (share_type IN ('user_device', 'server', 'recovery'))
);
-- Indexes for account_shares
CREATE INDEX idx_account_shares_account_id ON account_shares(account_id);
CREATE INDEX idx_account_shares_party_id ON account_shares(party_id);
CREATE INDEX idx_account_shares_active ON account_shares(account_id, is_active) WHERE is_active = TRUE;
-- Account Recovery Sessions table
CREATE TABLE account_recovery_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
account_id UUID NOT NULL REFERENCES accounts(id),
recovery_type VARCHAR(20) NOT NULL, -- 'device_lost', 'share_rotation'
old_share_type VARCHAR(20),
new_keygen_session_id UUID,
status VARCHAR(20) NOT NULL,
requested_at TIMESTAMP NOT NULL DEFAULT NOW(),
completed_at TIMESTAMP,
CONSTRAINT chk_recovery_status CHECK (status IN ('requested', 'in_progress', 'completed', 'failed'))
);
-- Indexes for account_recovery_sessions
CREATE INDEX idx_account_recovery_account_id ON account_recovery_sessions(account_id);
CREATE INDEX idx_account_recovery_status ON account_recovery_sessions(status);
-- ============================================
-- Audit Service Schema
-- ============================================
-- Audit Workflows table
CREATE TABLE audit_workflows (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_name VARCHAR(255) NOT NULL,
workflow_type VARCHAR(50) NOT NULL,
data_hash BYTEA NOT NULL,
threshold_n INTEGER NOT NULL,
threshold_t INTEGER NOT NULL,
sign_session_id UUID, -- Related signing session
signature BYTEA,
status VARCHAR(20) NOT NULL,
created_by VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
expires_at TIMESTAMP,
completed_at TIMESTAMP,
metadata JSONB,
CONSTRAINT chk_audit_workflow_status CHECK (status IN ('pending', 'in_progress', 'approved', 'rejected', 'expired'))
);
-- Indexes for audit_workflows
CREATE INDEX idx_audit_workflows_status ON audit_workflows(status);
CREATE INDEX idx_audit_workflows_created_at ON audit_workflows(created_at);
CREATE INDEX idx_audit_workflows_workflow_type ON audit_workflows(workflow_type);
-- Audit Approvers table
CREATE TABLE audit_approvers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workflow_id UUID NOT NULL REFERENCES audit_workflows(id) ON DELETE CASCADE,
approver_id VARCHAR(255) NOT NULL,
party_id VARCHAR(255) NOT NULL,
party_index INTEGER NOT NULL,
status VARCHAR(20) NOT NULL,
approved_at TIMESTAMP,
comments TEXT,
CONSTRAINT chk_approver_status CHECK (status IN ('pending', 'approved', 'rejected')),
UNIQUE(workflow_id, approver_id)
);
-- Indexes for audit_approvers
CREATE INDEX idx_audit_approvers_workflow_id ON audit_approvers(workflow_id);
CREATE INDEX idx_audit_approvers_approver_id ON audit_approvers(approver_id);
CREATE INDEX idx_audit_approvers_status ON audit_approvers(status);
-- ============================================
-- Shared Audit Logs Schema
-- ============================================
-- Audit Logs table (shared across all services)
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
service_name VARCHAR(100) NOT NULL,
action_type VARCHAR(100) NOT NULL,
user_id VARCHAR(255),
resource_type VARCHAR(100),
resource_id VARCHAR(255),
session_id UUID,
ip_address INET,
user_agent TEXT,
request_data JSONB,
response_data JSONB,
status VARCHAR(20) NOT NULL,
error_message TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CONSTRAINT chk_audit_status CHECK (status IN ('success', 'failure', 'pending'))
);
-- Indexes for audit_logs
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
CREATE INDEX idx_audit_logs_user_id ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_session_id ON audit_logs(session_id);
CREATE INDEX idx_audit_logs_action_type ON audit_logs(action_type);
CREATE INDEX idx_audit_logs_service_name ON audit_logs(service_name);
-- Partitioning for audit_logs (if needed for large scale)
-- CREATE TABLE audit_logs_y2024m01 PARTITION OF audit_logs
-- FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
-- ============================================
-- Helper Functions
-- ============================================
-- Function to update updated_at timestamp
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ language 'plpgsql';
-- Triggers for auto-updating updated_at
CREATE TRIGGER update_mpc_sessions_updated_at
BEFORE UPDATE ON mpc_sessions
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_accounts_updated_at
BEFORE UPDATE ON accounts
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_audit_workflows_updated_at
BEFORE UPDATE ON audit_workflows
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Function to cleanup expired sessions
CREATE OR REPLACE FUNCTION cleanup_expired_sessions()
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
UPDATE mpc_sessions
SET status = 'expired', updated_at = NOW()
WHERE expires_at < NOW()
AND status IN ('created', 'in_progress');
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ language 'plpgsql';
-- Function to cleanup old messages
CREATE OR REPLACE FUNCTION cleanup_old_messages(retention_hours INTEGER DEFAULT 24)
RETURNS INTEGER AS $$
DECLARE
deleted_count INTEGER;
BEGIN
DELETE FROM mpc_messages
WHERE created_at < NOW() - (retention_hours || ' hours')::INTERVAL;
GET DIAGNOSTICS deleted_count = ROW_COUNT;
RETURN deleted_count;
END;
$$ language 'plpgsql';
-- Comments
COMMENT ON TABLE mpc_sessions IS 'MPC session management - Coordinator does not participate in MPC computation';
COMMENT ON TABLE participants IS 'Session participants - tracks join status of each party';
COMMENT ON TABLE mpc_messages IS 'MPC protocol messages - encrypted, router does not decrypt';
COMMENT ON TABLE party_key_shares IS 'Server party key shares - encrypted storage of tss-lib data';
COMMENT ON TABLE accounts IS 'User accounts with MPC-based authentication';
COMMENT ON TABLE audit_logs IS 'Comprehensive audit trail for all operations';