What are Secrets?
Secrets are random 32-byte values that serve as cryptographic keys for cross-chain swaps. They enable atomic swaps by:
- Locking funds in escrows via hash commitments
- Authorizing withdrawals when revealed
- Ensuring atomicity across chains
Critical: Once a secret is revealed on-chain, anyone can see it. Never reveal secrets until you’ve verified escrow deployments are correct.
Secret Generation
Requirements
Secrets must be:
- Exactly 32 bytes (64 hex characters)
- Cryptographically random
- Unique for each order (or each fill in multi-fill orders)
Generating Secure Secrets
Use Node.js crypto.randomBytes for cryptographically secure randomness:
// From tests/utils/secret.ts:4-6
export function getSecret(): string {
return add0x(randomBytes(32).toString('hex'))
}
Complete example:
import { randomBytes } from 'crypto'
// Generate a single secret
const secret = '0x' + randomBytes(32).toString('hex')
// Example output: '0x1234...abcd' (66 characters including 0x)
// Generate multiple secrets for multi-fill order
const secrets = Array.from({ length: 5 }, () =>
'0x' + randomBytes(32).toString('hex')
)
Never use Math.random() or other non-cryptographic random number generators for secrets. They are predictable and can be exploited.
Secret Hashing
Before creating an order, secrets must be hashed:
// From hash-lock.ts:17-24
public static hashSecret(secret: string): string {
assert(
isHexBytes(secret) && getBytesCount(secret) === 32n,
'secret length must be 32 bytes hex encoded'
)
return keccak256(secret)
}
Usage:
import { HashLock } from '@1inch/cross-chain-sdk'
const secret = '0x' + randomBytes(32).toString('hex')
const secretHash = HashLock.hashSecret(secret)
// Secret hash is used in order creation
const secretHashes = secrets.map(s => HashLock.hashSecret(s))
The hash is what gets committed to the blockchain. The actual secret remains private until revelation.
When to Reveal Secrets
The timing of secret revelation is critical for security and successful swaps.
Revelation Workflow
Checking Readiness
Use the SDK to check when escrows are deployed and ready:
// Poll for ready escrows
while (true) {
const secretsToShare = await sdk.getReadyToAcceptSecretFills(orderHash)
if (secretsToShare.fills.length > 0) {
// Escrows are deployed and ready
break
}
await sleep(1000) // Wait 1 second
}
Verification Before Reveal
Always verify these parameters before revealing secrets:
- Escrow addresses match expected values
- Token addresses are correct
- Token amounts are correct
- Time-locks are acceptable
- Safety deposits are sufficient
Example verification:
const secretsToShare = await sdk.getReadyToAcceptSecretFills(orderHash)
for (const fill of secretsToShare.fills) {
// Verify escrow parameters
const isValid = (
fill.srcEscrowAddress === expectedSrcAddress &&
fill.dstEscrowAddress === expectedDstAddress &&
fill.srcToken === expectedSrcToken &&
fill.dstToken === expectedDstToken &&
fill.amount >= expectedAmount
)
if (!isValid) {
console.error('Escrow parameters mismatch!', fill)
continue // Don't reveal this secret
}
// Safe to reveal
await sdk.submitSecret(orderHash, secrets[fill.idx])
}
Submitting Secrets
// From README.md:205-207 (example pattern)
await sdk.submitSecret(hash, secrets[idx])
console.log({idx}, 'shared secret')
Complete example:
import { SDK, OrderStatus } from '@1inch/cross-chain-sdk'
// Submit secrets as escrows become ready
const alreadyShared = new Set<number>()
while (true) {
const secretsToShare = await sdk.getReadyToAcceptSecretFills(orderHash)
for (const fill of secretsToShare.fills) {
const idx = fill.idx
// Don't submit the same secret twice
if (alreadyShared.has(idx)) continue
// Verify escrow deployment (see above)
const isValid = verifyEscrowDeployment(fill)
if (!isValid) continue
// Reveal the secret
await sdk.submitSecret(orderHash, secrets[idx])
alreadyShared.add(idx)
console.log(`Revealed secret ${idx}`)
}
// Check if order is complete
const { status } = await sdk.getOrderStatus(orderHash)
if (
status === OrderStatus.Executed ||
status === OrderStatus.Expired ||
status === OrderStatus.Refunded
) {
break
}
await sleep(1000)
}
Security Best Practices
Storage
-
Never log secrets in production environments
// BAD - Secrets in logs!
console.log('Generated secret:', secret)
// GOOD - Log without exposing secret
console.log('Generated secret hash:', HashLock.hashSecret(secret))
-
Store in memory only when possible
// Generate secrets right before order creation
const secrets = Array.from({ length: count }, getSecret)
// Use immediately
const hashLock = HashLock.forMultipleFills(
HashLock.getMerkleLeaves(secrets)
)
// Keep in memory until swap completes
-
Encrypt if persisting to disk or database
import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'
function encryptSecret(secret: string, key: Buffer): string {
const iv = randomBytes(16)
const cipher = createCipheriv('aes-256-gcm', key, iv)
const encrypted = Buffer.concat([
cipher.update(secret, 'utf8'),
cipher.final()
])
return JSON.stringify({
iv: iv.toString('hex'),
encrypted: encrypted.toString('hex'),
tag: cipher.getAuthTag().toString('hex')
})
}
Generation
-
Use cryptographic RNG
// GOOD
import { randomBytes } from 'crypto'
const secret = '0x' + randomBytes(32).toString('hex')
// BAD - Predictable!
const badSecret = '0x' + Math.random().toString(16).repeat(4)
-
Generate sufficient entropy
// Ensure exactly 32 bytes
const secret = '0x' + randomBytes(32).toString('hex')
assert(secret.length === 66, 'Invalid secret length')
-
Generate unique secrets per order
// Each order needs fresh secrets
function createOrder() {
const secrets = Array.from({ length: count }, getSecret)
// ... create order with these secrets
}
Revelation
-
Verify before revealing
// Always check escrow parameters match expectations
if (!verifyEscrowDeployment(fill)) {
throw new Error('Escrow verification failed - not revealing secret')
}
-
Reveal at the right time
// Wait for escrows to be deployed
const fills = await sdk.getReadyToAcceptSecretFills(orderHash)
// Verify we're in the right time-lock stage
const timeLocks = order.timeLocks.toSrcTimeLocks(deployedAt)
if (timeLocks.isFinalityLock()) {
// Too early - wait for finality lock to end
return
}
-
Handle revelation failures
try {
await sdk.submitSecret(orderHash, secret)
} catch (error) {
// Log error but keep secret for retry
console.error('Failed to submit secret:', error.message)
// Retry later...
}
Multi-Fill Orders
For orders with multiple fills, manage secrets carefully:
import { HashLock } from '@1inch/cross-chain-sdk'
import { randomBytes } from 'crypto'
// Generate secrets
const secretCount = 5
const secrets = Array.from({ length: secretCount }, () =>
'0x' + randomBytes(32).toString('hex')
)
// Create Merkle tree
const leaves = HashLock.getMerkleLeaves(secrets)
const hashLock = HashLock.forMultipleFills(leaves)
// Track which secrets have been revealed
const revealed = new Set<number>()
// Reveal secrets as fills happen
for (const fill of secretsToShare.fills) {
if (!revealed.has(fill.idx)) {
// Verify this specific fill
if (verifyFill(fill)) {
await sdk.submitSecret(orderHash, secrets[fill.idx])
revealed.add(fill.idx)
}
}
}
Common Mistakes to Avoid
-
Revealing secrets too early
// BAD - Don't reveal immediately after order creation
await sdk.submitOrder(...)
await sdk.submitSecret(hash, secret) // Escrows not deployed yet!
// GOOD - Wait for escrows
await sdk.submitOrder(...)
const fills = await sdk.getReadyToAcceptSecretFills(hash)
if (fills.fills.length > 0) {
await sdk.submitSecret(hash, secret)
}
-
Not verifying escrow parameters
// BAD - Blindly revealing
await sdk.submitSecret(hash, secret)
// GOOD - Verify first
const fills = await sdk.getReadyToAcceptSecretFills(hash)
for (const fill of fills.fills) {
if (verifyEscrowDeployment(fill)) {
await sdk.submitSecret(hash, secrets[fill.idx])
}
}
-
Using predictable secrets
// BAD - Predictable pattern
const secret = '0x' + '1234567890'.repeat(7)
// GOOD - Cryptographically random
const secret = '0x' + randomBytes(32).toString('hex')
-
Logging secrets in production
// BAD
console.log('Secret:', secret)
// GOOD - Log hash instead
console.log('Secret hash:', HashLock.hashSecret(secret))
Secret Lifecycle Summary
| Phase | Action | Security Consideration |
|---|
| Generation | Create random 32-byte value | Use crypto.randomBytes |
| Hashing | Compute keccak256(secret) | Verify 32-byte input |
| Commitment | Include hash in order | Hash is public, secret stays private |
| Storage | Keep secret in memory/encrypted | Never log or expose |
| Verification | Check escrow deployments | Verify all parameters |
| Revelation | Submit to SDK | Only after verification |
| Cleanup | Clear from memory | Prevent memory leaks |
Example: Complete Secrets Flow
import { SDK, HashLock, OrderStatus } from '@1inch/cross-chain-sdk'
import { randomBytes } from 'crypto'
async function executeSwap() {
// 1. Generate secrets
const secrets = Array.from({ length: 3 }, () =>
'0x' + randomBytes(32).toString('hex')
)
// 2. Create hash lock
const hashLock = HashLock.forMultipleFills(
HashLock.getMerkleLeaves(secrets)
)
// 3. Hash secrets for submission
const secretHashes = secrets.map(s => HashLock.hashSecret(s))
// 4. Create and submit order
const { hash } = await sdk.createOrder(quote, {
walletAddress,
hashLock,
preset: 'fast',
source: 'my-app',
secretHashes
})
await sdk.submitOrder(srcChainId, order, quoteId, secretHashes)
// 5. Wait and reveal secrets
const revealed = new Set<number>()
while (true) {
const fills = await sdk.getReadyToAcceptSecretFills(hash)
for (const fill of fills.fills) {
if (revealed.has(fill.idx)) continue
// 6. VERIFY escrow deployment
const isValid = (
fill.srcEscrowAddress === expectedSrcAddress &&
fill.dstEscrowAddress === expectedDstAddress
// ... other checks
)
if (!isValid) {
console.error('Invalid escrow parameters!')
continue
}
// 7. Reveal secret
await sdk.submitSecret(hash, secrets[fill.idx])
revealed.add(fill.idx)
console.log(`Revealed secret ${fill.idx}`)
}
// 8. Check completion
const { status } = await sdk.getOrderStatus(hash)
if (status === OrderStatus.Executed) {
console.log('Swap completed!')
break
}
await sleep(1000)
}
// 9. Clear secrets from memory
secrets.fill('0x' + '0'.repeat(64))
}