Skip to content

Commit fcbf3ab

Browse files
authored
Merge pull request #11 from BlockstreamResearch/spec
Migration from FORS+C to PORS+FP
2 parents 0390761 + b482bbc commit fcbf3ab

26 files changed

Lines changed: 2216 additions & 160 deletions
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
\section{Motivation}
2+
Current Bitcoin signature schemes (ECDSA\cite{ecdsa_fips186} and Schnorr\cite{bip340} over secp256k1) are vulnerable to quantum attacks via Shor's algorithm\cite{shor1994algorithms, shor1997polynomial}. Hash-based signature schemes offer post-quantum security relying only on the security of cryptographic hash functions, which are believed to resist quantum attacks apart from Grover's quadratic speedup (mitigated by doubling hash output lengths).
3+
4+
The key challenges for deploying hash-based signatures in Bitcoin are:
5+
\begin{enumerate}
6+
\item \textbf{Signature size}: NIST-standardized SLH-DSA signatures \cite{slhdsa_fips205} range from 7KB to 50KB, significantly larger than current 64-byte Schnorr signatures. This increases transaction fees and reduces the network's throughput.
7+
\item \textbf{Stateful vs. Stateless tradeoff}: Stateful schemes (e.g., XMSS \cite{nist_sp800_208}) offer compact signatures but require careful state management to prevent key reuse. Additionally, Bitcoin users expect to restore wallets from static seed phrases (BIP-39\cite{bip39}). Pure stateful schemes break this model since signing state cannot be recovered from a seed alone, the signer must know which one-time keys have already been used. Stateless schemes (e.g., SLH-DSA) are robust but produce large signatures.
8+
\end{enumerate}
9+
10+
\subsection{SHRINCS}
11+
SHRINCS (Shrunken SHPINCS) is a hybrid post-quantum signature scheme that combines the efficiency of stateful signatures with the robustness of stateless ones. It provides with two parameters set depending on what matters for verifier:
12+
\begin{itemize}
13+
\item SHRINCS-B (Bitcoin-optimized): Minimizes signature size at the cost of more verification hashes.
14+
\item SHRINCS-L (Liquid-optimized): Minimizes verification cost at the cost of larger signatures.
15+
\end{itemize}
16+
The advantages of SHRINCS are following:
17+
\begin{itemize}
18+
\item \textbf{Efficient stateful path.} During normal operation with intact state, SHRINCS uses an Unbalanced XMSS (UXMSS) tree with WOTS+C one-time signatures. Signature sizes and verification costs are:
19+
\begin{itemize}
20+
\item SHRINCS-B: \textbf{only} \texttt{308 + 16q} bytes and \texttt{2042 + q} hashes, which makes this setting practical from the \textit{witness size} perspective
21+
\item SHRINCS-L: \texttt{1076 + 16q} bytes and \textbf{only} \texttt{54 + q} hashes, which makes this setting practical from the \textit{verification complexity} perspective
22+
\end{itemize}
23+
bytes (where \texttt{q >= 1} is the signature counter).
24+
\item \textbf{Robust stateless fallback.} When the state is lost, corrupted, or exhausted, SHRINCS falls back to a SPHINCS+-based stateless signature (2856 or 4392 bytes). This is still smaller than standard SLH-DSA.
25+
\item \textbf{Static seed backup.} Users can back up their wallet using a standard seed phrase. Upon recovery, the wallet operates in stateless-only mode, preserving funds while accepting larger signatures.
26+
\item \textbf{The number of supported signatures}. The construction supports up to 159 (SHRINCS-B) and 207 (SHRINCS-L) stateful signatures and up to $2^{20}$ stateless signatures, which should be enough for wallets.
27+
\end{itemize}
28+
29+
\subsection{Limitations and Practical Considerations}
30+
Users and developers should be aware of the following limitations inherent to stateful signature schemes.
31+
32+
\paragraph{State Management Across Devices.} The signing state (specifically, the counter \texttt{q} indicating which one-time keys have been already used) cannot be safely shared across multiple devices without coordination. Two devices signing with the same \texttt{q} value would reuse a one-time signature, which leads to key compromise. Some mitigation strategies can be:
33+
\begin{itemize}
34+
\item Dedicate disjoint ranges of leaf indices to different devices.
35+
\item Use a single signing device with secure state storage (other devices are stateless backups).
36+
\item Accept stateless-only operation for multi-device wallets.
37+
\end{itemize}
38+
39+
\paragraph{State Corruption and Power Failure.}
40+
If a signing operation is interrupted (e.g., due to power failure) after producing a signature but before persisting the updated state, the signer may not know whether the one-time key was used. Subsequent signing attempts risk catastrophic key reuse. Approaches to mitigate:
41+
\begin{itemize}
42+
\item Incrementing the state counter before the signature. If signing fails, a leaf is wasted but security is preserved (we utilize this approach in the specification).
43+
\item State corruption detection, to maintain checksums or redundant state copies to detect corruption. If corruption is detected, fall back to stateless mode.
44+
\item Other state and backup risks and management techniques are described in \cite{draft-ietf-pquip-hbs-state-03}.
45+
\end{itemize}
46+
47+
\paragraph{State Growth with Multiple Addresses.}
48+
Each SHRINCS-based address requires its own signing state. As users generate new addresses (as recommended for privacy), they must maintain state for each address that may be used for future spending. This state must be preserved for as long as the address holds funds. Mitigation:
49+
\begin{itemize}
50+
\item Wallet backups must include current state for all active addresses, not just the seed.
51+
\item Addresses can be marked as "stateless-only" after backup, accepting larger signatures for those addresses.
52+
\end{itemize}
53+
54+
\paragraph{Variable Stateful Signature Sizes.}
55+
SHRINCS signatures presume variable signature size and verification complexity depending on: (1) the number of already produced stateful signatures; (2) the signing mode. In decentralized system we need to consider how the cost of the signature verification is calculated to prevent spam attacks.
56+
\begin{itemize}
57+
\item That's basically the reason this specification introduced two modes for SHRINCS. SHRINCS-B is designed for accounting systems, where the user pays for each transaction byte, so the signature size matters. SHRINCS-L is better suited to systems that calculate transaction cost based on the number of computation units consumed.
58+
\item If such or a similar post-quantum signature construction is accepted in Bitcoin, it's likely to be done with one (e.g. \texttt{OP\_SHRINCS}) or the set (e.g. \texttt{OP\_WOTS, OP\_FORS, OP\_XMSS}) of opcodes, 1 byte per each. If the signature presumes a dynamic number of hashes to be verified (and the cost of verifying "short" and "long" chains is the same), it can lead to spam with longer verification paths.
59+
\item In the case of systems where the fee additionally is calculated based on the computation complexity of the transaction, we could implement a kind of efficient pre-compiles, but in the case of dynamic signatures, we need to introduce additional parameters.
60+
\end{itemize}
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
\section{SHRINCS}
2+
This section specifies the complete SHRINCS signature scheme by composing the building blocks defined in previous sections.
3+
4+
\subsection{Key Generation}
5+
The \texttt{SHRINCS.KeyGen} function generates a fresh key pair and initializes the signing state. It samples a 3n-byte random seed, partitions it into \texttt{SK.seed} (for key derivation), \texttt{SK.prf} (for message randomization), and \texttt{PK.seed} (for domain separation). It then computes \texttt{PK.sf} (the stateful UXMSS root) and \texttt{PK.sl} (the stateless hypertree root), combining them into \texttt{PK.root} via a tweakable hash. The initial state sets \texttt{q=0} with \texttt{valid=true}, indicating the stateful path is available starting from signature number 1.
6+
7+
\begin{verbatim}
8+
Algorithm: SHRINCS.KeyGen()
9+
Output:
10+
SK: secret key
11+
PK: public key
12+
state: initial signing state
13+
14+
1. seed ←$ {0,1}^(3n)
15+
2. SK.seed ← seed[0:n]
16+
3. SK.prf ← seed[n:2n]
17+
4. PK.seed ← seed[2n:3n]
18+
5. ADRS ← new_ADRS()
19+
6. PK.sf ← uxmss_root(SK.seed, PK.seed, ADRS)
20+
7. setLayerAddress(ADRS, d - 1)
21+
8. setTreeAddress(ADRS, 0)
22+
9. PK.sl ← xmss_root(SK.seed, PK.seed, ADRS, h_prime)
23+
10. setLayerAddress(ADRS, 0)
24+
11. setTreeAddress(ADRS, 0)
25+
12. setTypeAndClear(ADRS, ROOT)
26+
13. PK.root ← Tw(PK.seed, ADRS, PK.sf || PK.sl)
27+
14. SK ← (SK.seed, SK.prf, PK.seed, PK.sf, PK.sl, PK.root)
28+
15. PK ← (PK.seed, PK.root)
29+
16. state ← { q: 0, valid: true }
30+
17. return (SK, PK, state)
31+
\end{verbatim}
32+
33+
\subsection{Recovery from Seed}
34+
The \texttt{SHRINCS.Restore} function recovers a key pair from a backed-up seed. This enables the standard Bitcoin workflow of restoring a wallet from a seed phrase. However, since the signing state (which \texttt{q} values have been used) cannot be recovered from the seed alone, the stateful path is disabled by setting \texttt{valid=false}. The recovered wallet can only use stateless signatures, which are larger but do not require state tracking. This is the key tradeoff that enables static backups while maintaining security.
35+
\begin{verbatim}
36+
Algorithm: SHRINCS.Restore(seed)
37+
Input:
38+
seed: master seed
39+
Output:
40+
SK: secret key
41+
PK: public key
42+
state: invalid state (stateless only)
43+
44+
1. SK.seed ← seed[0:n]
45+
2. SK.prf ← seed[n:2n]
46+
3. PK.seed ← seed[2n:3n]
47+
4. ADRS ← new_ADRS()
48+
5. PK.sf ← uxmss_root(SK.seed, PK.seed, ADRS)
49+
6. setLayerAddress(ADRS, d - 1)
50+
7. setTreeAddress(ADRS, 0)
51+
8. PK.sl ← xmss_root(SK.seed, PK.seed, ADRS, h_prime)
52+
9. setLayerAddress(ADRS, 0)
53+
10. setTreeAddress(ADRS, 0)
54+
11. setTypeAndClear(ADRS, ROOT)
55+
12. PK.root ← Tw(PK.seed, ADRS, PK.sf || PK.sl)
56+
13. SK ← (SK.seed, SK.prf, PK.seed, PK.sf, PK.sl, PK.root)
57+
14. PK ← (PK.seed, PK.root)
58+
15. state ← { q: false, valid: false }
59+
16. return (SK, PK, state)
60+
\end{verbatim}
61+
62+
\subsection{Stateful signing}
63+
The \texttt{SHRINCS.SignStateful} function signs a message using the efficient stateful (UXMSS) path. It first checks that state is valid and that signature slots remain (\texttt{q <= hsf + 1}). It then increments \texttt{q} and updates the state before generating the signature. This ordering is critical: if the process is interrupted after signing but before state persistence, the one-time key would be consumed without the state reflecting it, risking catastrophic key reuse on retry (see Section~1.2).
64+
65+
The signature includes \texttt{PK.sl} prepended to enable unified verification. This produces minimal signatures (324 bytes for q = 1 in SHRINCS-B) that grow linearly with \texttt{q}. If state is invalid or exhausted, the function returns "false" and the caller should use stateless signing instead.
66+
\begin{verbatim}
67+
Algorithm: SHRINCS.SignStateful(m, SK, state)
68+
Input:
69+
m: message to sign
70+
SK: secret key (SK.seed, SK.prf, PK.seed, PK.sf, PK.sl, PK.root)
71+
state: current signing state { q, valid }
72+
Output:
73+
sig: stateful signature, or $\bot$ if state is invalid/exhausted
74+
state_prime: updated state
75+
76+
1. if not state.valid:
77+
return (false, state)
78+
2. q ← state.q + 1
79+
3. if q > hsf + 1:
80+
return (false, state)
81+
4. state_prime ← { q: q, valid: true }
82+
5. ADRS ← new_ADRS()
83+
6. uxmss_sig ← uxmss_sign(m, SK.seed, SK.prf, PK.seed, PK.root, ADRS, q)
84+
7. sig ← PK.sl || uxmss_sig
85+
8. return (sig, state_prime)
86+
\end{verbatim}
87+
88+
\subsection{Stateless signing}
89+
The \texttt{SHRINCS.SignStateless} function signs a message using the stateless (SPHINCS+C) path. This is used when state is lost, corrupted, or exhausted.
90+
91+
SHRINCS uses a unified digest for the stateless path: a single call to \texttt{H\_msg\_fors} produces a digest that encodes both the FORS leaf indices (determining which leaves to reveal in the few-time signature) and the hypertree tree/leaf indices (determining which XMSS trees and WOTS+C key pairs to use at each layer). The grinding counter \texttt{ctr} is iterated until the grinding condition is satisfied on this unified digest.
92+
93+
The digest output length is \texttt{(roundup(k*a + hsl) / 8)} bytes. The first \texttt{k*a} bits encode the \texttt{k} FORS leaf indices (with the last a bits required to be zero by grinding). The remaining \texttt{hsl} bits encode the hypertree path indices.
94+
95+
The signature is constructed bottom-up: first a FORS+C signature on the message, then \texttt{d} XMSS signatures on successive layer roots ascending through the hypertree. The signature includes \texttt{PK.sf} prepended to enable unified verification.
96+
97+
\begin{verbatim}
98+
Algorithm: SHRINCS.SignStateless(m, SK)
99+
Input:
100+
m: message to sign
101+
SK: secret key
102+
Output:
103+
sig: stateless signature
104+
105+
1. ADRS ← new_ADRS()
106+
2. setLayerAddress(ADRS, 0)
107+
3. setTreeAddress(ADRS, 0)
108+
4. (fors_sig, digest) ← fors_sign(m, SK.seed, SK.prf, PK.seed, PK.root, ADRS)
109+
5. (tree_idx, leaf_idx) ← parse_idx(digest)
110+
6. indices ← fors_msg_to_indices(digest)
111+
7. setLayerAddress(ADRS, 0)
112+
8. setTreeAddress(ADRS, tree_idx[0] * 2^h_prime + leaf_idx[0])
113+
9. fors_pk ← fors_pkFromSig(fors_sig, indices, PK.seed, ADRS)
114+
10. ht_sig ← []
115+
11. msg ← fors_pk
116+
12. for layer from 0 to d - 1:
117+
a. setLayerAddress(ADRS, layer)
118+
b. setTreeAddress(ADRS, tree_idx[layer])
119+
c. xmss_sig ← xmss_sign(msg, SK.seed, SK.prf, PK.seed, PK.root, ADRS, h', leaf_idx[layer])
120+
d. ht_sig ← ht_sig || xmss_sig
121+
e. if layer < d - 1:
122+
msg ← xmss_root(SK.seed, PK.seed, ADRS, h') // we may cache or reuse the root computed during xmss_sign
123+
13. sig ← PK.sf || fors_sig || ht_sig
124+
14. return sig
125+
\end{verbatim}
126+
127+
\subsection{Index Parsing}
128+
The \texttt{parse\_idx} function extracts tree and leaf indices for each hypertree layer from the stateless digest. In the unified digest scheme, these bits follow the \texttt{k*a} bits used for FORS indices. The \texttt{hsl} bits are partitioned into \texttt{d} pairs of \texttt{(leaf\_idx, tree\_idx)}, extracted from least significant (layer 0) to most significant (layer \texttt{d-1}), with \texttt{h'} bits per leaf index.
129+
%\mknote{This is a bit confusin, that we extract the indices of the OTS/FTS schemes used, but not the indices of the revealed leaves in FORS. With the Hashing changes, it needs a double-check that everything is computed correctly and the same data is not used for two purposes. }
130+
\begin{verbatim}
131+
Algorithm: parse_idx(digest)
132+
Input:
133+
digest: message digest
134+
Output:
135+
tree_idx: array of d tree indices
136+
leaf_idx: array of d leaf indices
137+
138+
1. ht_offset ← k * a
139+
2. ht_bits ← extract_bits(digest, ht_offset, hsl)
140+
3. idx ← ht_bits
141+
4. tree_idx ← []
142+
5. leaf_idx ← []
143+
6. for layer from 0 to d - 1:
144+
a. leaf_idx[layer] ← idx AND (2^h_prime - 1)
145+
b. idx ← idx >> h_prime
146+
c. tree_idx[layer] ← idx
147+
7. return (tree_idx, leaf_idx)
148+
\end{verbatim}
149+
\paragraph{Note:} The total digest must encode \texttt{k*a + hsl = 6*22 + 24 = 156} bits. The \texttt{H\_msg\_fors} output length (Section~5.4.2) must be set accordingly: \texttt{roundup((k*a + hsl) / 8) = 20} bytes. This is a correction from the original \texttt{roundup((k*a) / 8) = 17} bytes, which was insufficient for the unified digest.
150+
151+
\subsection{Stateful verification}
152+
The \texttt{SHRINCS.VerifyStateful} function verifies a stateful signature. It extracts \texttt{PK.sl} from the signature, parses the UXMSS signature, and infers \texttt{q} from the authentication path length. It then uses the two-phase WOTS+C hashing to recover \texttt{PK.sf} via \texttt{uxmss\_pkFromSig} and verifies that \texttt{Tw(PK.seed, ADRS, PK.sf || PK.sl)} equals \texttt{PK.root}.
153+
\begin{verbatim}
154+
Algorithm: SHRINCS.VerifyStateful(m, sig, PK)
155+
Input:
156+
m: message
157+
sig: stateful signature
158+
PK: public key
159+
Output:
160+
valid: boolean
161+
162+
1. ADRS ← new_ADRS()
163+
2. PK.sl ← sig[0 : n]
164+
3. uxmss_sig ← sig[n : ]
165+
4. wots_sig_size ← R_SIZE + c + l * n
166+
5. auth_len ← len(uxmss_sig) - wots_sig_size
167+
6. if auth_len < 0 or (auth_len mod n) != 0:
168+
return false
169+
7. q_raw ← auth_len / n
170+
8. if q_raw < 1 or q_raw > hsf:
171+
return false
172+
9. wots_sig ← uxmss_sig[0 : wots_sig_size]
173+
10. auth ← uxmss_sig[wots_sig_size : ]
174+
11. if q_raw < hsf:
175+
candidates ← {q_raw}
176+
else:
177+
candidates ← {hsf, hsf + 1}
178+
12. for each q in candidates:
179+
a. ADRS ← new_ADRS()
180+
b. PK.sf ← uxmss_pkFromSig(wots_sig, auth, m, PK.seed, PK.root, ADRS, q)
181+
c. if PK.sf == false:
182+
continue
183+
d. setLayerAddress(ADRS, 0)
184+
setTreeAddress(ADRS, 0)
185+
setTypeAndClear(ADRS, ROOT)
186+
expected_root ← Tw(PK.seed, ADRS, PK.sf || PK.sl)
187+
e. if expected_root == PK.root:
188+
return true
189+
13. return false
190+
\end{verbatim}
191+
192+
\subsection{Stateless verification}
193+
The \texttt{SHRINCS.VerifyStateless} function verifies a stateless signature. It extracts \texttt{PK.sf} from the signature, recomputes the unified digest from the FORS+C signature's \texttt{R} and \texttt{ctr}, extracts both FORS indices and hypertree indices from the same digest, then verifies bottom-up through the hypertree.
194+
\begin{verbatim}
195+
Algorithm: SHRINCS.VerifyStateless(m, sig, PK)
196+
Input:
197+
m: message
198+
sig: stateless signature
199+
PK: public key
200+
Output:
201+
valid: boolean
202+
203+
1. ADRS ← new_ADRS()
204+
2. PK.sf ← sig[0 : n]
205+
3. offset ← n
206+
4. fors_sig_size ← R_SIZE + (k - 1) * (n + a * n)
207+
5. fors_sig ← sig[offset : offset + fors_sig_size]
208+
6. offset ← offset + fors_sig_size
209+
7. R ← fors_sig[0 : R_SIZE]
210+
8. setTypeAndClear(ADRS, SL_H_MSG)
211+
9. digest ← H_msg_fors(ADRS, R, PK.seed, PK.root, m)
212+
10. indices ← fors_msg_to_indices(digest)
213+
11. if indices[k-1] != 0:
214+
return false
215+
12. (tree_idx, leaf_idx) ← parse_idx(digest)
216+
13. setLayerAddress(ADRS, 0)
217+
14. setTreeAddress(ADRS, tree_idx[0] * 2^h_prime + leaf_idx[0])
218+
15. fors_pk ← fors_pkFromSig(fors_sig, indices, PK.seed, ADRS)
219+
16. msg ← fors_pk
220+
17. xmss_sig_size ← R_SIZE + c + l * n + h_prime * n
221+
18. for layer from 0 to d - 1:
222+
a. xmss_sig ← sig[offset : offset + xmss_sig_size]
223+
b. offset ← offset + xmss_sig_size
224+
c. wots_sig ← xmss_sig[0 : R_SIZE + c + l * n]
225+
d. auth ← xmss_sig[R_SIZE + c + l * n : R_SIZE + c + l * n + h' * n]
226+
e. setLayerAddress(ADRS, layer)
227+
f. setTreeAddress(ADRS, tree_idx[layer])
228+
g. msg ← xmss_pkFromSig(wots_sig, auth, msg, PK.seed, PK.root, ADRS, h_prime, leaf_idx[layer])
229+
h. if msg == false:
230+
return false
231+
19. PK.sl ← msg
232+
20. setLayerAddress(ADRS, 0)
233+
21. setTreeAddress(ADRS, 0)
234+
22. setTypeAndClear(ADRS, ROOT)
235+
23. expected_root ← Tw(PK.seed, ADRS, PK.sf || PK.sl)
236+
24. return (expected_root == PK.root)
237+
\end{verbatim}
238+
239+
\subsection{Unified Verification}
240+
The \texttt{SHRINCS.Verify} function provides a single entry point for verifying either stateful or stateless signatures. It distinguishes between the two types based on signature length: stateful signatures have a maximum size of \texttt{max\_sf\_size = n + R\_SIZE + c + l*n + hsf*n} bytes (both \texttt{q = hsf} and \texttt{q = hsf + 1} produce hsf auth-path nodes), while stateless signatures are strictly larger (see Section~8.7 for the derivation that this bound always holds).
241+
\begin{verbatim}
242+
Algorithm: SHRINCS.Verify(m, sig, PK)
243+
Input:
244+
m: message
245+
sig: signature
246+
PK: public key
247+
Output:
248+
valid: boolean
249+
250+
1. max_sf_size ← n + R_SIZE + c + l * n + hsf * n
251+
2. if len(sig) <= max_sf_size:
252+
return SHRINCS.VerifyStateful(m, sig, PK)
253+
else:
254+
return SHRINCS.VerifyStateless(m, sig, PK)
255+
\end{verbatim}

0 commit comments

Comments
 (0)