Skip to main content

What is a Hash Lock?

A hash lock is a cryptographic commitment scheme used to secure atomic swaps. It works by:
  1. Generating a random secret (32 bytes)
  2. Computing the hash of that secret using keccak256
  3. 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:
  1. Commitment: Maker commits to a secret without revealing it
  2. Authorization: Only someone with the secret can unlock funds
  3. 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

MethodDescription
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

MethodDescription
toString()Serialize to hex string
toBuffer()Serialize to Buffer
getPartsCount()Get fill count (multi-fill only)
eq(other)Check equality

Best Practices

  1. Generate cryptographically random secrets
    // Good: Use crypto.randomBytes
    const secret = '0x' + randomBytes(32).toString('hex')
    
    // Bad: Don't use Math.random() or predictable values
    
  2. 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
  3. Verify order parameters before revealing secrets
    • Check escrow addresses match expectations
    • Verify token amounts and addresses
    • Confirm time-locks are acceptable
  4. Choose appropriate fill strategy
    • Single-fill: Simpler, lower gas costs
    • Multi-fill: Better for large orders, enables partial fills

Build docs developers (and LLMs) love