This document describes the multi-sig (multi-signature) implementation for StellarFlow that allows two or more oracle servers to collectively sign and approve price updates before they are submitted to the Soroban contract on Stellar.
-
MultiSigService (
src/services/multiSigService.ts)- Manages multi-sig price requests
- Handles signature collection and aggregation
- Communicates with remote servers for signature requests
- Tracks signature expiration
-
MultiSigSubmissionService (
src/services/multiSigSubmissionService.ts)- Background job that polls for approved multi-sig prices
- Automatically submits approved prices to Stellar
- Handles retries and error logging
-
MarketRateService (
src/services/marketRate/marketRateService.ts)- Enhanced to support multi-sig workflow
- Routes prices through multi-sig process when enabled
- Asynchronously requests remote signatures
-
StellarService (
src/services/stellarService.ts)- New method:
submitMultiSignedPriceUpdate() - Combines multiple signatures into a single Stellar transaction
- Handles fee bumping and retries for multi-signed transactions
- New method:
-
PriceUpdates Routes (
src/routes/priceUpdates.ts)- API endpoints for multi-sig operations
- Remote signature request endpoint:
POST /api/price-updates/sign - Status monitoring endpoints
Three new models added to prisma/schema.prisma:
-
MultiSigPrice: Tracks multi-sig price approval requests
- Stores currency, rate, required/collected signatures
- Tracks expiration and submission status
-
MultiSigSignature: Individual signatures from signers
- Records signer identity and timestamp
- Stores signature in hex format
-
MultiSigPrice Relationship: Links to price review records for audit trail
Add the following to your .env file to enable multi-sig:
# Enable multi-signature mode (required to activate feature)
MULTI_SIG_ENABLED=true
# Number of signatures required before submission (default: 2)
MULTI_SIG_REQUIRED_COUNT=2
# Comma-separated list of remote oracle server URLs
# These servers will be requested to sign price updates
REMOTE_ORACLE_SERVERS=http://oracle-2.example.com:3000,http://oracle-3.example.com:3000
# Authentication token for remote signature requests
# All servers should use the same token for inter-server communication
MULTI_SIG_AUTH_TOKEN=your-secure-auth-token
# Polling interval for multi-sig submission service (milliseconds)
# How often to check for approved prices to submit
MULTI_SIG_POLL_INTERVAL_MS=30000
# Name/identifier for this oracle server
# Used in signature records for tracking
ORACLE_SIGNER_NAME=oracle-server-1Server 1 (Primary):
ORACLE_SECRET_KEY=SBXXX...
MULTI_SIG_ENABLED=true
MULTI_SIG_REQUIRED_COUNT=2
REMOTE_ORACLE_SERVERS=http://server2.internal:3000
MULTI_SIG_AUTH_TOKEN=shared-secret-token
ORACLE_SIGNER_NAME=oracle-primaryServer 2 (Secondary):
ORACLE_SECRET_KEY=SBYYY...
MULTI_SIG_ENABLED=true
MULTI_SIG_REQUIRED_COUNT=2
REMOTE_ORACLE_SERVERS=http://server1.internal:3000
MULTI_SIG_AUTH_TOKEN=shared-secret-token
ORACLE_SIGNER_NAME=oracle-secondary1. Price Fetched
↓
2. Price Reviewed (anomaly detection)
↓
3. If AUTO_APPROVED:
├─ Multi-Sig Request Created
├─ Local Server Signs
└─ Remote Signatures Requested (async)
↓
4. Remote Servers Receive Request via:
POST /api/price-updates/sign
↓
5. Remote Servers Sign and Return Signature
↓
6. Once All Signatures Collected:
├─ MultiSigPrice marked as APPROVED
├─ Background job detects APPROVED status
└─ Multiple Signatures Added to Transaction
↓
7. Transaction Submitted to Stellar
↓
8. Confirmation Recorded
- MarketRateService fetches rate
- PriceReviewService evaluates for anomalies
- If manual review needed → PENDING
- If approved → proceed to multi-sig
- MultiSigService creates MultiSigPrice record
- Sets required signature count (default: 2)
- Sets expiration time (1 hour)
- Local server immediately signs the price update
- Signature stored in MultiSigSignature table
- Collected signatures incremented
// Non-blocking request to remote servers
// Each remote server independently signs
POST /api/price-updates/sign
{
"multiSigPriceId": 123,
"currency": "NGN",
"rate": 1234.56,
"source": "CoinGecko",
"memoId": "SF-NGN-1234567890",
"signerPublicKey": "GXXXXXX..."
}- Remote servers receive request on their
/api/price-updates/signendpoint - They verify authorization token
- They sign the price data (deterministic message format)
- They return signature
- Requesting server records remote signature
- Once all signatures collected → MultiSigPrice.status = "APPROVED"
- MultiSigSubmissionService polls for APPROVED prices
- All signatures added to Stellar transaction
- Transaction submitted with multiple signatures
- After confirmation on Stellar:
- MultiSigPrice.submittedAt set
- Linked PriceReviewService record marked as SUBMITTED
- OnChainPrice record created
POST /api/price-updates/multi-sig/request
Content-Type: application/json
{
"priceReviewId": 123,
"currency": "NGN",
"rate": 1234.56,
"source": "CoinGecko",
"memoId": "SF-NGN-1234567890"
}
Response:
{
"success": true,
"data": {
"multiSigPriceId": 456,
"currency": "NGN",
"rate": 1234.56,
"requiredSignatures": 2
}
}POST /api/price-updates/sign
Authorization: Bearer your-secure-auth-token
Content-Type: application/json
{
"multiSigPriceId": 456,
"currency": "NGN",
"rate": 1234.56,
"source": "CoinGecko",
"memoId": "SF-NGN-1234567890",
"signerPublicKey": "GXXXXXX..."
}
Response:
{
"success": true,
"data": {
"multiSigPriceId": 456,
"signature": "hex_encoded_signature",
"signerPublicKey": "GXXXXXX...",
"signerName": "oracle-secondary"
}
}GET /api/price-updates/multi-sig/456/status
Response:
{
"success": true,
"data": {
"id": 456,
"currency": "NGN",
"rate": 1234.56,
"status": "APPROVED",
"collectedSignatures": 2,
"requiredSignatures": 2,
"expiresAt": "2024-03-27T16:30:00Z",
"signers": [
{
"publicKey": "GXXXXXX...",
"name": "oracle-primary",
"signedAt": "2024-03-27T15:30:00Z"
},
{
"publicKey": "GYYYYYYY...",
"name": "oracle-secondary",
"signedAt": "2024-03-27T15:30:05Z"
}
]
}
}GET /api/price-updates/multi-sig/456/signatures
Response:
{
"success": true,
"data": {
"multiSigPriceId": 456,
"currency": "NGN",
"rate": 1234.56,
"signatures": [
{
"signerPublicKey": "GXXXXXX...",
"signerName": "oracle-primary",
"signature": "hex_encoded_signature_1"
},
{
"signerPublicKey": "GYYYYYYY...",
"signerName": "oracle-secondary",
"signature": "hex_encoded_signature_2"
}
]
}
}GET /api/price-updates/multi-sig/signer-info
Response:
{
"success": true,
"data": {
"publicKey": "GXXXXXX...",
"name": "oracle-primary"
}
}GET /api/price-updates/multi-sig/pending
Response:
{
"success": true,
"data": [
{
"id": 456,
"currency": "NGN",
"rate": 1234.56,
"status": "PENDING",
"collectedSignatures": 1,
"requiredSignatures": 2,
"expiresAt": "2024-03-27T16:30:00Z",
"signerCount": 1
}
]
}POST /api/price-updates/multi-sig/456/record-submission
Content-Type: application/json
{
"memoId": "SF-NGN-1234567890",
"stellarTxHash": "abc123def456..."
}
Response:
{
"success": true
}- All inter-server communication requires
MULTI_SIG_AUTH_TOKEN - Implement HTTPS in production for all server-to-server calls
- Use unique token per deployment environment
- Deterministic message format ensures all servers sign same data
- Format:
SF-PRICE-<CURRENCY>-<RATE>-<SOURCE> - Public keys verified on Stellar network
- Multi-sig requests expire after 1 hour by default
- Prevents old signatures from being used
- Background cleanup job removes expired records
- Consider implementing rate limiting on
POST /api/price-updates/sign - Prevents signature endpoint from being abused
Set MULTI_SIG_ENABLED=false to revert to single-signature mode:
- Prices submitted immediately after approval
- No remote signature requests
- Direct submission to Stellar
- Code maintains backward compatibility
- Existing single-sig endpoints still function
- Can use multi-sig URLs even if disabled (returns appropriate errors)
Monitor logs for:
[MultiSig] Created signature request 123 for NGN rate 1234.56
[MultiSig] Added signature 2/2 for MultiSigPrice 123
[MultiSig] MultiSigPrice 123 is now APPROVED
[MultiSigSubmissionService] Successfully submitted multi-sig price 123
Find pending multi-sig prices:
SELECT * FROM "MultiSigPrice" WHERE status = 'PENDING';Check signatures for a price:
SELECT * FROM "MultiSigSignature" WHERE "multiSigPriceId" = 123;Find expired requests:
SELECT * FROM "MultiSigPrice"
WHERE status = 'PENDING' AND "expiresAt" < NOW();Issue: Remote signature requests failing
- Verify
REMOTE_ORACLE_SERVERSURLs are correct - Check
MULTI_SIG_AUTH_TOKENmatches on all servers - Ensure network connectivity between servers
- Check firewall rules allow inter-server communication
Issue: Signatures not being collected
- Verify remote server's
/api/price-updates/signendpoint is working - Check logs for "Failed to request signature from..."
- Ensure
MULTI_SIG_ENABLED=trueon remote server
Issue: Prices stuck in PENDING status
- Check if required signature count is too high
- Verify all remote servers are running and healthy
- Check expiration times
# Check for approved prices every 10 seconds (faster)
MULTI_SIG_POLL_INTERVAL_MS=10000
# Check every 60 seconds (slower, less resource usage)
MULTI_SIG_POLL_INTERVAL_MS=60000Current: 1 hour (3600000 ms)
Adjust in MultiSigService constructor if needed
All multi-sig tables have appropriate indexes:
multiSigPrice(status, expiresAt)multiSigSignature(multiSigPriceId)
-
Variable Signature Thresholds
- Support weighted voting (e.g., 2-of-3)
- Different thresholds per currency
-
Distributed Consensus
- Byzantine Fault Tolerance
- Timeout handling for non-responsive servers
-
Signature Caching
- Pre-sign common price updates
- Reduce round-trip time
-
WebSocket Updates
- Real-time multi-sig status via Socket.io
- Dashboard integration
-
Audit Trail
- More detailed logging of signature process
- Compliance reporting