AIP: 0006
Title: FlightLock — On-Chain TOTP Verification (OP_CHECKTOTP) for Avian
Author: Craig Donnachie <craig.donnachie@gmail.com>
Status: Draft
Type: Consensus (soft fork)
Created: 2025-08-11
License: BSD-2-Clause
This AIP introduces FlightLock, a consensus feature by introducing a new opcode, OP_CHECKTOTP, that verifies RFC‑6238 TOTP codes on‑chain using Median‑Time‑Past (MTP) as the time base. It enables P2SH/P2WSH scripts that require a valid authenticator app code plus a signature to spend cold‑storage UTXOs.
Cold‑storage spends are planned and infrequent. Requiring a TOTP code from an offline device adds a strong second factor without any server. Doing the validation in Script removes trust in wallet software and prevents bypass via off‑chain mistakes.
HOTP (HMAC‑based One‑Time Password, RFC 4226) generates codes from a shared secret K and an incrementing counter C:
HOTP(K, C) = Truncate( HMAC(K, C) ) mod 10^digits
The counter increments for each new code. Validation requires the verifier to track and synchronize the counter.
TOTP (Time‑based One‑Time Password, RFC 6238) is built on HOTP but uses time to derive the counter:
T = floor( (current_time − T0) / step )
TOTP(K) = HOTP(K, T)
This removes the need to store counters; both sides just need reasonably synchronized clocks. OP_CHECKTOTP computes HOTP with a time‑derived counter using the blockchain’s MTP.
Stack before (top → bottom):
<K_i> <code> <hashK_i> <t0> <step> <digits> <skew> <algo>
Where:
K_i= per‑UTXO TOTP key (raw bytes)code= integer from authenticator apphashK_i= SHA256(K_i) committed in scriptt0, step, digits, skew, algo= minimally‑encoded integers
Operation:
- Key binding: require
SHA256(K_i) == hashK_i. - Time source: compute epoch index
T = floor((MTP(parent_of_spend_block) − t0) / step)where MTP is the Median‑Time‑Past of the parent block (BIP113‑style). - Window: for
w ∈ {−skew … +skew}, compute codeC_wper RFC‑6238 (HOTP truncate) with HMAC(algo,K_i,T+w) and accept if any match. - Result: push
1on success, else0.
Fail if digits/step/skew/algo are out of bounds or if any pop/parse step underflows the stack.
Argument bounds / policy:
digits ∈ {6,8}step ∈ [15,120]secondsskew ≤ 1algo ∈ {0=HMAC‑SHA1, 1=HMAC‑SHA256, 2=HMAC‑SHA512}
Transactions are non‑standard if arguments are outside these bounds. Standard relay requires spend in P2WSH; non‑segwit spends may be non‑standard if witness/script size is large.
2‑factor single‑sig
OP_SHA256 <hashK_i> OP_SWAP OP_SWAP
<t0> <step> <digits> <skew> <algo>
OP_CHECKTOTP OP_VERIFY
OP_DUP OP_HASH160 <RecipientPKH> OP_EQUALVERIFY
OP_CHECKSIG
Spend stack: <sig> <pubkey> <K_i> <code>
With timeout recovery
OP_IF
OP_SHA256 <hashK_i> OP_SWAP OP_SWAP
<t0> <step> <digits> <skew> <algo>
OP_CHECKTOTP OP_VERIFY
OP_DUP OP_HASH160 <RecipientPKH> OP_EQUALVERIFY OP_CHECKSIG
OP_ELSE
<locktime> OP_CHECKLOCKTIMEVERIFY OP_DROP
OP_DUP OP_HASH160 <RecoveryPKH> OP_EQUALVERIFY OP_CHECKSIG
OP_ENDIF
Wallets MUST NOT reuse K_i. Revealing K_i on spend is expected. Derive a unique K_i per UTXO from a master secret K_master:
K_i = HKDF‑SHA256(IKM = K_master,
info = "FlightLock" || outpoint || scriptHash,
L = 20 or 32) # 20 for SHA1, 32 for SHA256/512
Wallets should present a QR for K_i compatible with standard authenticator apps.
- VersionBits: use bit 26 (bit 27 = dual‑algo; bit 28 = FlightPath SPV)
- Threshold: 90% over a 2016‑block window
- Activation: BIP9‑style state machine
- Miner influence: using MTP(parent) limits single‑miner clock control.
- Replay/guessing: 6/8‑digit window with
skew ≤ 1and per‑UTXO keys make brute force infeasible; signature required. - Key reuse: must derive unique
K_iper UTXO to avoid cross‑UTXO compromise. - Loss of authenticator: recovery path with CLTV is recommended.
- Interpreter: implement constant‑time HMAC for SHA1/256/512 and HOTP truncate; expose parent block MTP to Script.
- Tests: RFC‑6238 vectors (SHA1/256/512, step=30, digits=6), window edge cases, invalid parameters, policy enforcement, segwit/non‑segwit spends.
Soft fork: older nodes remain compatible; upgraded miners enforce opcode rules.
center|700px
End of AIP‑0006