LPsLux Proposals
Threshold Cryptography
LP-6475

LuxDA Sealed Messages

Draft

LuxDA Sealed Messages specification for LuxDA Bus

Category
Core
Created
2026-01-02

Abstract

This LP defines sealed message mode for LuxDA, where message content is encrypted under threshold TFHE and only revealed when specified conditions are met. This enables time-lock encryption, commit-reveal schemes, and other sealed-bid patterns.

Motivation

Sealed messages enable:

  1. Time-Lock: Content revealed after time
  2. Commit-Reveal: Commit now, reveal later
  3. Sealed Bids: Bid without revealing amount
  4. Dead Man's Switch: Reveal if no heartbeat

Specification

1. Sealed Message Structure

1.1 Sealed Envelope

type SealedMessage struct {
    // Envelope metadata (public)
    Version       uint8
    NamespaceId   [20]byte
    Epoch         uint64
    SenderPubKey  []byte

    // Encrypted payload key
    EncryptedKey []byte  // TFHE encrypted AES key

    // AES-encrypted content
    EncryptedContent []byte
    ContentMAC       []byte

    // Reveal conditions
    RevealConditions *RevealConditions

    // Signature
    Signature []byte
}

1.2 Two-Layer Encryption

Original Content
       ↓
  AES-256-GCM (with random key K)
       ↓
  Encrypted Content
       +
  Key K encrypted under TFHE (to committee)
       ↓
  Sealed Message

2. Reveal Conditions

2.1 Condition Types

type RevealConditions struct {
    // Primary condition
    Primary RevealCondition

    // Alternative conditions (OR)
    Alternatives []RevealCondition

    // Required signatures for override
    OverrideThreshold uint32
    OverrideSigners   []Identity
}

type RevealCondition struct {
    Type      ConditionType
    Params    []byte
    Deadline  uint64  // Must be met by this time, or message expires
}

type ConditionType uint8
const (
    // Time-based
    ConditionAfterTimestamp  ConditionType = 1
    ConditionAfterBlock      ConditionType = 2
    ConditionAfterDuration   ConditionType = 3

    // Event-based
    ConditionOnEvent         ConditionType = 4
    ConditionOnInclusion     ConditionType = 5

    // Approval-based
    ConditionNofMApproval    ConditionType = 6
    ConditionSenderApproval  ConditionType = 7

    // Absence-based (dead man's switch)
    ConditionNoHeartbeat     ConditionType = 8
)

2.2 Time-Lock Condition

type TimeLockParams struct {
    ReleaseTime uint64
}

func CheckTimeLock(params []byte) bool {
    var tlp TimeLockParams
    cbor.Unmarshal(params, &tlp)
    return uint64(time.Now().Unix()) >= tlp.ReleaseTime
}

2.3 Dead Man's Switch

type DeadManSwitchParams struct {
    HeartbeatInterval uint64
    LastHeartbeat     uint64
}

func CheckDeadManSwitch(params []byte, state *SealedMessageState) bool {
    var dms DeadManSwitchParams
    cbor.Unmarshal(params, &dms)

    lastHB := state.LastHeartbeat
    return uint64(time.Now().Unix()) > lastHB + dms.HeartbeatInterval
}

func SendHeartbeat(sealedMsgID [32]byte, senderSig []byte) error {
    // Update last heartbeat
    state := getSealedMessageState(sealedMsgID)
    state.LastHeartbeat = uint64(time.Now().Unix())
    return nil
}

3. Sealing Process

3.1 Seal Message

func SealMessage(
    content []byte,
    conditions *RevealConditions,
    nsId [20]byte,
    epoch uint64,
) (*SealedMessage, error) {
    // Generate random AES key
    aesKey := make([]byte, 32)
    rand.Read(aesKey)

    // Encrypt content with AES
    nonce := make([]byte, 12)
    rand.Read(nonce)
    aead, _ := cipher.NewGCM(aes.NewCipher(aesKey))
    encryptedContent := aead.Seal(nonce, nonce, content, nil)

    // Encrypt AES key under TFHE
    pk, _ := keyRegistry.GetPublicKey(nsId, epoch)
    encryptedKey := tfhe.EncryptBytes(pk, aesKey)

    return &SealedMessage{
        Version:          1,
        NamespaceId:      nsId,
        Epoch:            epoch,
        EncryptedKey:     encryptedKey,
        EncryptedContent: encryptedContent,
        RevealConditions: conditions,
    }, nil
}

4. Unsealing Process

4.1 Request Unseal

type UnsealRequest struct {
    SealedMsgRef  [32]byte
    Requester     Identity
    ConditionMet  ConditionType
    ConditionProof []byte
    Signature     []byte
}

func RequestUnseal(sealed *SealedMessage, conditionProof []byte) (*UnsealRequest, error) {
    // Identify which condition is met
    metCondition := findMetCondition(sealed.RevealConditions, conditionProof)
    if metCondition == nil {
        return nil, ErrNoConditionMet
    }

    return &UnsealRequest{
        SealedMsgRef:   computeSealedMsgRef(sealed),
        ConditionMet:   metCondition.Type,
        ConditionProof: conditionProof,
    }, nil
}

4.2 Unseal Execution

func Unseal(sealed *SealedMessage, shares []*DecryptionShare) ([]byte, error) {
    // Combine shares to decrypt TFHE key
    decryptedKey, err := CombineDecryptShares(shares, threshold)
    if err != nil {
        return nil, err
    }

    // Extract AES key
    aesKey := tfhe.DecodeBytes(decryptedKey)

    // Decrypt content
    nonce := sealed.EncryptedContent[:12]
    ciphertext := sealed.EncryptedContent[12:]
    aead, _ := cipher.NewGCM(aes.NewCipher(aesKey))

    plaintext, err := aead.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return nil, ErrDecryptionFailed
    }

    return plaintext, nil
}

5. Sealed Message Lifecycle

5.1 State Machine

Created → Pending → [Condition Met] → Unsealing → Revealed
                 → [Expired] → Expired
                 → [Cancelled] → Cancelled

5.2 State Transitions

type SealedMessageState struct {
    Status        SealedStatus
    CreatedAt     uint64
    LastHeartbeat uint64
    UnsealedAt    uint64
    ExpiredAt     uint64
}

type SealedStatus uint8
const (
    SealedPending   SealedStatus = 1
    SealedUnsealing SealedStatus = 2
    SealedRevealed  SealedStatus = 3
    SealedExpired   SealedStatus = 4
    SealedCancelled SealedStatus = 5
)

6. Sender Controls

6.1 Cancel Sealed Message

type CancelRequest struct {
    SealedMsgRef [32]byte
    Reason       string
    SenderSig    []byte
}

func CancelSealed(req *CancelRequest) error {
    state := getSealedMessageState(req.SealedMsgRef)

    // Only pending messages can be cancelled
    if state.Status != SealedPending {
        return ErrCannotCancel
    }

    // Verify sender signature
    if !verifySenderSignature(req) {
        return ErrInvalidSignature
    }

    // Check cancellation allowed by conditions
    if !canCancel(state) {
        return ErrCancellationNotAllowed
    }

    state.Status = SealedCancelled
    return nil
}

6.2 Update Conditions

func UpdateConditions(
    sealedMsgRef [32]byte,
    newConditions *RevealConditions,
    senderSig []byte,
) error {
    // Verify sender
    // Check update allowed
    // Apply new conditions
    // Note: Cannot make conditions less restrictive
}

7. UX Constraints

7.1 Explicit Opt-In

// Sealed mode requires explicit namespace flag
type NamespacePolicy struct {
    SealedModeAllowed bool  // Must be true
}

7.2 Visibility

// Recipients see sealed envelope, not content
type SealedMessageView struct {
    From       Identity
    SentAt     uint64
    Status     SealedStatus
    Conditions *RevealConditions  // Public
    Preview    []byte            // Optional encrypted preview
}

7.3 Expiration

All sealed messages MUST have expiration:

const (
    MaxSealDuration = 365 * 24 * time.Hour  // 1 year max
    DefaultExpiry   = 30 * 24 * time.Hour   // 30 days default
)

8. Use Cases

8.1 Time-Locked Will

func CreateTimeLockWill(content []byte, releaseDate time.Time) (*SealedMessage, error) {
    conditions := &RevealConditions{
        Primary: RevealCondition{
            Type:   ConditionAfterTimestamp,
            Params: encodeTimestamp(releaseDate),
        },
        Alternatives: []RevealCondition{
            {
                Type:   ConditionNofMApproval,
                Params: encodeApprovers(executors, 2, 3),
            },
        },
    }

    return SealMessage(content, conditions, willNsId, currentEpoch)
}

8.2 Sealed Bid

func CreateSealedBid(amount uint64, auctionId [32]byte) (*SealedMessage, error) {
    conditions := &RevealConditions{
        Primary: RevealCondition{
            Type:   ConditionOnEvent,
            Params: encodeBidRevealEvent(auctionId),
        },
    }

    bidContent := encodeBid(amount, auctionId)
    return SealMessage(bidContent, conditions, auctionNsId, currentEpoch)
}

8.3 Dead Man's Switch

func CreateDeadManSwitch(content []byte, checkIn time.Duration) (*SealedMessage, error) {
    conditions := &RevealConditions{
        Primary: RevealCondition{
            Type: ConditionNoHeartbeat,
            Params: encodeHeartbeatParams(checkIn),
        },
        Alternatives: []RevealCondition{
            {
                Type:   ConditionSenderApproval,
                Params: nil,
            },
        },
    }

    return SealMessage(content, conditions, dmsNsId, currentEpoch)
}

Rationale

Why Two-Layer Encryption?

  • TFHE ciphertext size independent of content
  • Efficient for large content
  • Standard pattern

Why Explicit Conditions?

  • Transparency about reveal rules
  • Auditable
  • Programmable

Why Max Duration?

  • Committee changes over time
  • Key rotation
  • Prevent stale sealed messages

Security Considerations

Key Escrow

Committee collectively holds unsealing power. Choose appropriate committee for trust model.

Premature Reveal

Mitigated by:

  • Condition verification
  • Multiple committee members
  • Honest threshold assumption

Denied Reveal

Mitigated by:

  • Liveness assumptions
  • Alternative conditions
  • Override mechanisms

Test Plan

Unit Tests

  1. Seal/Unseal: Round-trip encryption
  2. Conditions: All condition types
  3. Lifecycle: State transitions

Integration Tests

  1. Time-Lock: Wait and reveal
  2. Dead Man's Switch: Heartbeat, timeout
  3. Multi-Condition: Alternative triggers

Security Tests

  1. Premature Unseal: Reject before condition
  2. Expired Message: Handle expiration
  3. Invalid Proofs: Reject bad condition proofs

References


LP-6475 v1.0.0 - 2026-01-02