Skip to main content

Commit Flow

The commit operation destroys tokens and stores a cryptographic commitment in the Merkle tree.

Committing native GHOST

interface ICommitRevealVault {
function commitNative(
bytes32 commitment,
bytes32 quantumCommitment
) external payable;
}

Send GHOST as msg.value. The vault burns the tokens and inserts the commitment.

Example with Foundry

# Commit 1 GHOST
cast send 0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70 \
"commitNative(bytes32,bytes32)" \
$COMMITMENT \
0x0000000000000000000000000000000000000000000000000000000000000000 \
--value 1ether \
--rpc-url https://testnet.specterchain.com \
--chain-id 5445 \
--private-key $PRIVATE_KEY

Example with ethers.js

const vault = new ethers.Contract(
'0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
['function commitNative(bytes32 commitment, bytes32 quantumCommitment) payable'],
signer
);

const tx = await vault.commitNative(
commitment, // Poseidon hash of your secret inputs
ethers.ZeroHash, // quantum commitment (0x0 if not using quantum resistance)
{ value: ethers.parseEther('1'), gasPrice: 1_000_000_000n }
);
await tx.wait();

Committing ERC20 tokens

interface ICommitRevealVault {
function commit(
address token,
uint256 amount,
bytes32 commitment,
bytes32 quantumCommitment
) external;
}

First approve the vault to spend your tokens, then call commit():

// Approve
const token = new ethers.Contract(tokenAddress, ['function approve(address,uint256)'], signer);
await (await token.approve('0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70', amount)).wait();

// Commit
const vault = new ethers.Contract(
'0x443434113980Ab9d5Eef0Ace7d1A29AB68Af6c70',
['function commit(address,uint256,bytes32,bytes32)'],
signer
);
await (await vault.commit(tokenAddress, amount, commitment, ethers.ZeroHash)).wait();

Committing with a policy

interface ICommitRevealVault {
function commitNativeWithPolicy(
bytes32 commitment,
bytes32 quantumCommitment,
address policy,
bytes32 policyParamsHash
) external payable;

function commitWithPolicy(
address token,
uint256 amount,
bytes32 commitment,
bytes32 quantumCommitment,
address policy,
bytes32 policyParamsHash
) external;
}

Policies bind enforceable conditions to the commitment. The policy contract's validate() function is called during reveal. See Policy System.

Computing the commitment

The commitment is a Poseidon hash of 5 or 7 fields:

commitment = Poseidon5(secret, nullifierSecret, tokenIdHash, amount, blinding)

Or with policy:

commitment = Poseidon7(secret, nullifierSecret, tokenIdHash, amount, blinding, policyId, policyParamsHash)

You can compute this client-side with snarkjs/circomlibjs, or via the commitment relayer:

curl -X POST https://relayer.specterchain.com/api/commitment/compute \
-H "Content-Type: application/json" \
-H "X-HMAC-Signature: $HMAC_SIG" \
-d '{
"secret": "0x...",
"nullifierSecret": "0x...",
"blinding": "0x...",
"tokenIdHash": "0x...",
"amount": "1000000000000000000"
}'

What happens on-chain

  1. The vault verifies the token is authorized via AssetGuard
  2. For ERC20: calls token.burn(from, amount). For native: calls precompile burnNativeFrom()
  3. Calls commitmentTree.insert(commitment) to add the leaf
  4. Records totalCommitted[token] += amount
  5. If policy is specified, stores commitmentPolicies[commitment] = policy
  6. Emits Committed(commitment, token, amount) or CommittedWithPolicy(...)

Rate limiting

A cooldown period (default 5 seconds) prevents rapid sequential commits from the same address. This is enforced via lastCommitTime mapping and commitCooldown parameter.