V2 Hard Fork Note (Radiant Core 2.1): At block 410,000, six new/re-enabled opcodes activate (OP_BLAKE3, OP_K12, OP_LSHIFT, OP_RSHIFT, OP_2MUL, OP_2DIV). dApps using these opcodes must ensure they are only used in transactions broadcast after the activation height. The minimum relay fee increases to 0.1 RXD/kB after block 415,000. Additionally,
OP_PUSH_TX_STATE(0xed) provides access to the current txid, total input sum, and total output sum — useful for fee verification and induction proof contracts.
Never rely on a single security control. Layer multiple defenses:
┌───────────────────────────────────────────┐
│ USER INTERFACE │
│ ┌───────────────────────────────────────┐│
│ │ INPUT VALIDATION ││
│ │ ┌───────────────────────────────────┐││
│ │ │ AUTHENTICATION │││
│ │ │ ┌───────────────────────────────┐│││
│ │ │ │ AUTHORIZATION ││││
│ │ │ │ ┌───────────────────────────┐││││
│ │ │ │ │ SMART CONTRACT │││││
│ │ │ │ │ ┌───────────────────────┐│││││
│ │ │ │ │ │ FUNDS ││││││
│ │ │ │ │ └───────────────────────┘│││││
│ │ │ │ └───────────────────────────┘││││
│ │ │ └───────────────────────────────┘│││
│ │ └───────────────────────────────────┘││
│ └───────────────────────────────────────┘│
└───────────────────────────────────────────┘
// ✅ GOOD: Request only needed permissions
const permissions = {
viewBalance: true,
signTransactions: true,
// NOT: fullWalletAccess: true
};
// ✅ GOOD: Limit transaction scope
const txLimits = {
maxAmount: 1000000, // 0.01 RXD max per tx
allowedAddresses: ['1Known...', '1Trusted...'],
requireConfirmation: true
};
// ✅ GOOD: Fail securely — don't expose internal details
async function processTransaction(tx) {
try {
await validateTransaction(tx);
await signTransaction(tx);
await broadcastTransaction(tx);
} catch (error) {
logger.error('Transaction failed', { txid: tx.id, error: error.message });
throw new UserFacingError('Transaction could not be completed.');
}
}
// ❌ BAD: Expose internal state
catch (error) {
throw new Error(`Failed: ${error.stack} - DB: ${dbConnection}`);
}
// ✅ GOOD: Validate everything — even from "trusted" sources
function processElectrumResponse(response) {
if (!isValidElectrumResponse(response)) throw new Error('Invalid response');
if (!isValidTxHex(response.tx)) throw new Error('Invalid tx hex');
if (response.amount < 0 || response.amount > MAX_SUPPLY) throw new Error('Invalid amount');
return sanitize(response);
}
// ❌ NEVER
localStorage.setItem('privateKey', privateKey.toWIF());
// ✅ GOOD: Encrypted storage with user password
async function storeKey(privateKey, password) {
const salt = crypto.getRandomValues(new Uint8Array(32));
const key = await deriveKey(password, salt);
const encrypted = await encrypt(privateKey.toWIF(), key);
await secureStorage.set('encryptedKey', { encrypted, salt });
}
// ✅ GOOD: BIP39/BIP44 derivation
function deriveKeys(mnemonic, accountIndex = 0) {
const seed = Mnemonic.fromPhrase(mnemonic);
const hdKey = seed.toHDPrivateKey();
const path = `m/44'/0'/${accountIndex}'/0`;
const account = hdKey.deriveChild(path);
return {
addresses: Array.from({ length: 20 }, (_, i) =>
account.deriveChild(i).publicKey.toAddress()
)
};
}
// ✅ GOOD: Clear sensitive data from memory
class SecureKey {
constructor(key) { this.keyBuffer = Buffer.from(key); }
destroy() {
crypto.getRandomValues(this.keyBuffer); // Overwrite with random
this.keyBuffer = new Uint8Array(0);
}
static async withKey(key, fn) {
const secureKey = new SecureKey(key);
try { return await fn(secureKey); }
finally { secureKey.destroy(); }
}
}
async function validateBeforeSigning(tx) {
// 1. Validate addresses
for (const output of tx.outputs) {
if (!Address.isValid(output.address)) throw new Error('Invalid address');
if (await isBlacklisted(output.address)) throw new Error('Blacklisted address');
}
// 2. Validate amounts
const totalOutput = tx.outputs.reduce((sum, o) => sum + o.value, 0);
const totalInput = tx.inputs.reduce((sum, i) => sum + i.value, 0);
if (totalOutput > totalInput) throw new Error('Output exceeds input');
// 3. Validate fee
const fee = totalInput - totalOutput;
const feeRate = fee / tx.getSize();
if (feeRate > MAX_REASONABLE_FEE_RATE) throw new Error('Unreasonably high fee');
if (fee < MIN_FEE) throw new Error('Fee too low');
// 4. Check for dust
for (const output of tx.outputs) {
if (output.value > 0 && output.value < DUST_LIMIT) throw new Error('Dust output');
}
}
// ✅ Always show transaction details before signing
async function confirmTransaction(tx) {
const details = {
recipientAddress: formatAddress(tx.outputs[0].address),
amount: formatAmount(tx.outputs[0].value),
fee: formatAmount(tx.getFee()),
warnings: []
};
if (tx.outputs[0].value > LARGE_TX_THRESHOLD)
details.warnings.push('Large transaction amount');
if (tx.getFee() > HIGH_FEE_THRESHOLD)
details.warnings.push('Higher than normal fee');
if (!await isKnownAddress(tx.outputs[0].address))
details.warnings.push('First time sending to this address');
return await showConfirmationDialog(details);
}
class UTXOManager {
private pendingUtxos = new Set();
async getAvailableUTXOs(address) {
const allUtxos = await electrum.listUnspent(address);
return allUtxos.filter(utxo => !this.pendingUtxos.has(`${utxo.txid}:${utxo.vout}`));
}
markAsPending(utxo) { this.pendingUtxos.add(`${utxo.txid}:${utxo.vout}`); }
}
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'wasm-unsafe-eval';
style-src 'self' 'unsafe-inline';
connect-src 'self' wss://electrumx.radiantexplorer.com:*;
img-src 'self' data: https:;
object-src 'none';
frame-ancestors 'none';
upgrade-insecure-requests;
">
// ✅ GOOD: Sanitize all user input before display
import DOMPurify from 'dompurify';
function displayUserContent(content) {
return DOMPurify.sanitize(content, { ALLOWED_TAGS: [] });
}
// ❌ BAD: Dangerous innerHTML
element.innerHTML = userProvidedContent;
async function copyToClipboard(text, type) {
await navigator.clipboard.writeText(text);
if (type === 'seed') {
setTimeout(async () => await navigator.clipboard.writeText(''), 60000);
showWarning('Seed phrase copied. Clipboard will be cleared in 60 seconds.');
}
}
// JWT with proper validation
function verifyToken(token) {
return jwt.verify(token, JWT_SECRET, {
issuer: 'radiant-dapp', audience: 'radiant-dapp-api',
algorithms: ['HS256'], maxAge: '1h'
});
}
// Rate limiting
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, max: 100,
message: 'Too many requests'
});
app.use('/api/', apiLimiter);
const transactionSchema = Joi.object({
recipientAddress: Joi.string()
.pattern(/^[13][a-km-zA-HJ-NP-Z1-9]{25,34}$/).required(),
amount: Joi.number().integer().min(546).max(2100000000000000).required(),
memo: Joi.string().max(256).optional()
});
// ✅ GOOD: Parameterized queries
const result = await db.query('SELECT * FROM transactions WHERE address = $1', [address]);
// ❌ BAD: SQL injection
const result = await db.query(`SELECT * FROM transactions WHERE address = '${address}'`);
// ✅ GOOD: Environment-based secrets — fail if missing
function requireEnv(name) {
const value = process.env[name];
if (!value) throw new Error(`Required env var ${name} is not set`);
return value;
}
const config = {
jwtSecret: requireEnv('JWT_SECRET'),
dbPassword: requireEnv('DB_PASSWORD'),
};
async function verifyContract(contractOutput, expectedTemplate) {
const codeScript = extractCodeScript(contractOutput.script);
const expectedCode = expectedTemplate.generateCode(parseContractParams(contractOutput.script));
if (!codeScript.equals(expectedCode)) throw new Error('Contract code mismatch');
return true;
}
async function executeContractCall(contract, functionName, args) {
const tx = await contract.functions[functionName](...args);
const simulation = await simulateTransaction(tx);
if (!simulation.success) throw new Error(`Simulation failed: ${simulation.error}`);
const confirmed = await confirmTransaction(tx, simulation);
if (!confirmed) throw new Error('User cancelled');
return await signAndBroadcast(tx);
}
class SecureElectrumClient {
async connect(host, port) {
const url = `wss://${host}:${port}`; // Use WSS (secure)
this.ws = new WebSocket(url);
// Verify server certificate on upgrade...
}
async request(method, params) {
const id = this.nextId++;
const response = await this.sendAndWait({ jsonrpc: '2.0', id, method, params });
if (response.id !== id) throw new Error('Response ID mismatch');
return response.result;
}
}
const OFFICIAL_DOMAINS = ['photonic.radiant4people.com', 'localhost'];
function verifyDomain() {
if (!OFFICIAL_DOMAINS.includes(window.location.hostname)) {
showPhishingWarning(window.location.hostname);
disableWalletOperations();
}
}
window.addEventListener('load', verifyDomain);
const warnings = {
largeAmount: (amount) => amount > 10000000000 ? 'Large transaction (>100 RXD)' : null,
newAddress: async (address) => !await hasTransactionHistory(address) ? 'New address' : null,
highFee: (feeRate) => feeRate > 10 ? 'Fee is higher than typical' : null,
contractInteraction: (script) => isComplexScript(script) ? 'Involves a smart contract' : null,
burnWarning: (outputs) => outputs.some(o => o.value === 0) ? 'May burn tokens' : null
};
Immediate (0-1 hour):
Short-term (1-24 hours):
Resolution (24-72 hours):
Post-Incident:
Email: info@radiantfoundation.org
Subject: [SECURITY] dApp Name - Brief Description
Security is everyone's responsibility. When in doubt, ask for review.