bolt8

Encrypted and authenticated transport, per BOLT #8.
git clone git://git.ppad.tech/bolt8.git
Log | Files | Refs | README | LICENSE

commit 2b87813654c338fef2cb90795ec24d031615df20
parent 4af1d8ed7eac2b14b8d4ada83183237f1625ac11
Author: Jared Tobin <jared@jtobin.io>
Date:   Sun, 25 Jan 2026 09:43:52 +0400

lib: add FrameResult and decrypt_frame_partial

Add recoverable partial framing support:
- FrameResult ADT with NeedMore, FrameOk, FrameError constructors
- decrypt_frame_partial returns NeedMore when buffer incomplete
- Useful for non-blocking I/O with incremental data arrival

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

Diffstat:
Mlib/Lightning/Protocol/BOLT8.hs | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 62 insertions(+), 0 deletions(-)

diff --git a/lib/Lightning/Protocol/BOLT8.hs b/lib/Lightning/Protocol/BOLT8.hs @@ -82,6 +82,8 @@ module Lightning.Protocol.BOLT8 ( , encrypt , decrypt , decrypt_frame + , decrypt_frame_partial + , FrameResult(..) -- * Errors , Error(..) @@ -123,6 +125,16 @@ data Error = | DecryptionFailed deriving (Eq, Show, Generic) +-- | Result of attempting to decrypt a frame from a partial buffer. +data FrameResult = + NeedMore {-# UNPACK #-} !Int + -- ^ More bytes needed; the 'Int' is the minimum additional bytes required. + | FrameOk !BS.ByteString !BS.ByteString !Session + -- ^ Successfully decrypted: plaintext, remainder, updated session. + | FrameError !Error + -- ^ Decryption failed with the given error. + deriving Generic + -- | Post-handshake session state. data Session = Session { sess_sk :: {-# UNPACK #-} !BS.ByteString -- ^ send key (32 bytes) @@ -608,6 +620,56 @@ decrypt_frame sess packet = do } pure (pt, remainder, sess') +-- | Decrypt a frame from a partial buffer, indicating when more data needed. +-- +-- Unlike 'decrypt_frame', this function handles incomplete buffers +-- gracefully by returning 'NeedMore' with the number of additional +-- bytes required to make progress. +-- +-- * If the buffer has fewer than 18 bytes (encrypted length + MAC), +-- returns @'NeedMore' n@ where @n@ is the bytes still needed. +-- * If the length header is complete but the body is incomplete, +-- returns @'NeedMore' n@ with bytes needed for the full frame. +-- * MAC or decryption failures return 'FrameError'. +-- * A complete, valid frame returns 'FrameOk' with plaintext, +-- remainder, and updated session. +-- +-- This is useful for non-blocking I/O where data arrives incrementally. +decrypt_frame_partial + :: Session + -> BS.ByteString -- ^ buffer (possibly incomplete) + -> FrameResult +decrypt_frame_partial sess buf + | buflen < 18 = NeedMore (18 - buflen) + | otherwise = + let !lc = BS.take 18 buf + !rest = BS.drop 18 buf + in case decrypt_with_ad (sess_rk sess) (sess_rn sess) BS.empty lc of + Nothing -> FrameError InvalidMAC + Just len_bytes -> case decode_be16 len_bytes of + Nothing -> FrameError InvalidLength + Just len -> + let !body_len = fi len + 16 + !(rn1, rck1, rk1) = step_nonce (sess_rn sess) + (sess_rck sess) (sess_rk sess) + in if BS.length rest < body_len + then NeedMore (body_len - BS.length rest) + else + let !bc = BS.take body_len rest + !remainder = BS.drop body_len rest + in case decrypt_with_ad rk1 rn1 BS.empty bc of + Nothing -> FrameError InvalidMAC + Just pt -> + let !(rn2, rck2, rk2) = step_nonce rn1 rck1 rk1 + !sess' = sess { + sess_rk = rk2 + , sess_rn = rn2 + , sess_rck = rck2 + } + in FrameOk pt remainder sess' + where + !buflen = BS.length buf + -- key rotation -------------------------------------------------------------- -- Key rotation occurs after nonce reaches 1000 (i.e., before using 1000)