What is a Hash Lock?
A hash lock is a cryptographic commitment scheme used to secure atomic swaps. It works by:
- Generating a random secret (32 bytes)
- Computing the hash of that secret using keccak256
- Locking funds that can only be unlocked by revealing the original secret
The 1inch SDK implements hash locks in the HashLock class:
// From hash-lock.ts:8-15
export class HashLock {
public static Web3Type = 'bytes32'
private readonly value: string
protected constructor(val: string) {
this.value = val
}
}
Hash-Lock Cryptography
Secret Hashing
Secrets must be exactly 32 bytes and are hashed using keccak256:
// 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)
}
Why keccak256? This cryptographic hash function is:
- One-way: Cannot derive the secret from the hash
- Deterministic: Same secret always produces same hash
- Collision-resistant: Nearly impossible to find two secrets with same hash
Security Properties
The hash lock provides:
- Commitment: Maker commits to a secret without revealing it
- Authorization: Only someone with the secret can unlock funds
- Atomicity: Same secret unlocks both source and destination escrows
Single-Fill vs Multi-Fill Orders
The SDK supports two types of orders:
Single-Fill Orders
For orders filled in one transaction, use a simple hash of the secret:
// From hash-lock.ts:63-68
/**
* Create HashLock from keccak256 hash of secret
*/
public static forSingleFill(secret: string): HashLock {
return new HashLock(HashLock.hashSecret(secret))
}
Example Usage:
import { HashLock } from '@1inch/cross-chain-sdk'
import { randomBytes } from 'crypto'
const secret = '0x' + randomBytes(32).toString('hex')
const hashLock = HashLock.forSingleFill(secret)
Multi-Fill Orders
For orders filled in multiple parts (partial fills), use a Merkle tree:
// From hash-lock.ts:70-82
public static forMultipleFills(leaves: MerkleLeaf[]): HashLock {
assert(
leaves.length > 2,
'leaves array must be greater than 2. Or use HashLock.forSingleFill'
)
const root = SimpleMerkleTree.of(leaves).root
const rootWithCount = BN.fromHex(root).setMask(
new BitMask(240n, 256n),
BigInt(leaves.length - 1)
)
return new HashLock(rootWithCount.toHex(64))
}
Why Merkle Trees?
- Each partial fill uses a different secret
- All secrets commit to a single Merkle root
- Proofs verify individual secrets belong to the set
Multi-fill orders enable better liquidity by allowing multiple resolvers to fill portions of large orders.
Merkle Tree Implementation
Generating Merkle Leaves
Each secret is combined with its index to create a unique leaf:
// From hash-lock.ts:32-42
public static getMerkleLeavesFromSecretHashes(
secretHashes: string[]
): MerkleLeaf[] {
return secretHashes.map(
(s, idx) =>
solidityPackedKeccak256(
['uint64', 'bytes32'],
[idx, s]
) as MerkleLeaf
)
}
The index prevents replay attacks - the same secret can’t be used for multiple fills.
Creating Merkle Proofs
When filling an order, the resolver needs a proof that their secret is valid:
// From hash-lock.ts:44-46
public static getProof(leaves: string[], idx: number): MerkleLeaf[] {
return SimpleMerkleTree.of(leaves).getProof(idx) as MerkleLeaf[]
}
Multi-Fill Interaction
The escrow factory creates interactions with Merkle proofs:
// From escrow-factory.ts:111-130
public getMultipleFillInteraction(
proof: MerkleLeaf[],
idx: number,
secretHash: string
): Interaction {
const data = AbiCoder.defaultAbiCoder().encode(
[
`(
bytes32[] proof,
uint256 idx,
bytes32 secretHash,
)`
],
[{proof, idx, secretHash}]
)
const dataNoOffset = add0x(data.slice(66))
return new Interaction(this.address.inner, dataNoOffset)
}
Parts Count Encoding
For multi-fill orders, the number of parts is encoded in the hash lock:
// From hash-lock.ts:84-91
/**
* Only use if HashLockInfo is for multiple fill order
* Otherwise garbage will be returned
*/
public getPartsCount(): bigint {
return new BN(BigInt(this.value)).getMask(new BitMask(240n, 256n)).value
}
The count is stored in the upper 16 bits (240-256) of the hash lock value.
Important: Only call getPartsCount() on multi-fill hash locks. Single-fill hash locks will return meaningless values.
Complete Example: Multi-Fill Order
import { HashLock, SDK } from '@1inch/cross-chain-sdk'
import { randomBytes } from 'crypto'
// Generate multiple secrets for partial fills
const secretCount = 5
const secrets = Array.from({ length: secretCount }, () =>
'0x' + randomBytes(32).toString('hex')
)
// Create Merkle tree hash lock
const leaves = HashLock.getMerkleLeaves(secrets)
const hashLock = HashLock.forMultipleFills(leaves)
console.log(`Hash lock for ${hashLock.getPartsCount()} parts`)
// Hash secrets for order submission
const secretHashes = secrets.map(s => HashLock.hashSecret(s))
// Create order
const { hash, order } = await sdk.createOrder(quote, {
walletAddress,
hashLock,
preset: 'fast',
source: 'my-app',
secretHashes
})
// Later: reveal secrets as fills happen
for (let idx = 0; idx < secrets.length; idx++) {
const secretsToShare = await sdk.getReadyToAcceptSecretFills(hash)
if (secretsToShare.fills.some(f => f.idx === idx)) {
await sdk.submitSecret(hash, secrets[idx])
console.log(`Revealed secret ${idx}`)
}
}
HashLock API Reference
Static Methods
| Method | Description |
|---|
forSingleFill(secret) | Create hash lock for single-fill order |
forMultipleFills(leaves) | Create hash lock for multi-fill order |
hashSecret(secret) | Hash a secret with keccak256 |
getMerkleLeaves(secrets) | Generate Merkle leaves from secrets |
getProof(leaves, idx) | Get Merkle proof for specific index |
fromString(value) | Deserialize from hex string |
fromBuffer(value) | Deserialize from Buffer |
Instance Methods
| Method | Description |
|---|
toString() | Serialize to hex string |
toBuffer() | Serialize to Buffer |
getPartsCount() | Get fill count (multi-fill only) |
eq(other) | Check equality |
Best Practices
-
Generate cryptographically random secrets
// Good: Use crypto.randomBytes
const secret = '0x' + randomBytes(32).toString('hex')
// Bad: Don't use Math.random() or predictable values
-
Store secrets securely until reveal time
- Never log secrets to console in production
- Use secure storage (encrypted, memory-only, etc.)
- Clear secrets from memory after use
-
Verify order parameters before revealing secrets
- Check escrow addresses match expectations
- Verify token amounts and addresses
- Confirm time-locks are acceptable
-
Choose appropriate fill strategy
- Single-fill: Simpler, lower gas costs
- Multi-fill: Better for large orders, enables partial fills