Files
zeekay 84f7a6380f chore: scrub 'Liquid EVM' from Envelope.OnchainTx doc comment
Replace 'Liquid EVM tx hash' with 'Lux EVM tx hash' — Lux esign
notarization is on Lux's chain, not the Liquidity-flavored EVM.
2026-06-10 12:25:51 -07:00

152 lines
5.1 KiB
Go

// Copyright 2026 Lux Industries Inc. and the luxfi/esign authors.
// Licensed under the Apache License, Version 2.0 (see LICENSE).
// Package envelope defines the canonical eSign envelope state machine.
//
// An envelope is the unit of signing: it bundles one or more PDF documents,
// a list of required signers, and an append-only audit chain. Envelopes
// progress through a fixed set of states; any unknown transition is an
// error. The state machine is independent of crypto and transport, so it
// can be exercised by unit tests without a signer or a network.
//
// Versions
//
// Every state-changing operation produces a new envelope version. Version
// N's hash is included in version N+1's audit event, forming a chain that
// makes any historical tamper detectable by comparing the current chain
// tip to the version most-recently observed by a third party.
package envelope
import (
"errors"
"time"
)
// State enumerates the legal positions in the envelope lifecycle.
//
// Draft — created, not yet sent for signature
// Sent — signers notified, awaiting first action
// Viewed — at least one signer has opened the document
// Signing — at least one signer has signed; not all required signers complete
// Completed — every required signer has signed successfully
// Voided — terminal: creator revoked the envelope before completion
// Expired — terminal: envelope passed its ExpiresAt without completing
// Declined — terminal: a required signer declined to sign
type State uint8
const (
StateDraft State = iota
StateSent
StateViewed
StateSigning
StateCompleted
StateVoided
StateExpired
StateDeclined
)
var stateNames = map[State]string{
StateDraft: "draft",
StateSent: "sent",
StateViewed: "viewed",
StateSigning: "signing",
StateCompleted: "completed",
StateVoided: "voided",
StateExpired: "expired",
StateDeclined: "declined",
}
// String renders the state in lowercase wire form ("draft", "sent", …).
func (s State) String() string {
if n, ok := stateNames[s]; ok {
return n
}
return "unknown"
}
// IsTerminal reports whether s is an end state — no further transitions allowed.
func (s State) IsTerminal() bool {
switch s {
case StateCompleted, StateVoided, StateExpired, StateDeclined:
return true
}
return false
}
// SignMethod tags how a signer's signature was produced. The choice affects
// which Signer/Verifier in pkg/sign is used; envelope.go is agnostic.
type SignMethod uint8
const (
MethodUnspecified SignMethod = iota
MethodEd25519 // stdlib crypto/ed25519
MethodMLDSA65 // post-quantum Dilithium (luxfi/keys)
MethodFROST // Schnorr threshold (luxfi/threshold)
MethodMPCECDSA // ECDSA via CGGMP21 (luxfi/mpc)
MethodECDSAP256 // stdlib crypto/ecdsa, P-256
MethodRSAPSS // stdlib crypto/rsa, PSS — DocuSign/Adobe interop
)
// SignerSlot describes one party who must sign before the envelope completes.
// SignerHash is the canonical 32-byte identifier — typically SHA-256 of the
// signer's public key, or for FROST/MPC, the hash of the group public key.
// Order=0 means "any sequence"; Order>=1 enforces strict sequential signing.
type SignerSlot struct {
SignerHash [32]byte
Method SignMethod
Required bool
Order uint8
}
// Signature is one signer's contribution to the envelope.
type Signature struct {
SignerHash [32]byte
Method SignMethod
SignedAt time.Time
Sig []byte // raw signature bytes; format depends on Method
PubKey []byte // signer pubkey (or group pubkey for threshold)
DocHash [32]byte
}
// AuditEvent records every observable transition. Hash chains backward to
// PrevHash so any state tampering is detected by re-hashing forward.
type AuditEvent struct {
Seq uint32
At time.Time
Actor [32]byte // hash of the principal causing the transition
From State
To State
Note string
PrevHash [32]byte // chain-back hash; zero on Seq=0
EventHash [32]byte // SHA-256 of canonical encoding of all fields above
}
// Envelope is the working representation of an eSign job.
type Envelope struct {
ID string
Version uint32
Document []byte // canonical PDF bytes
DocHash [32]byte
Signers []SignerSlot
Signatures []Signature
State State
CreatedAt time.Time
UpdatedAt time.Time
ExpiresAt time.Time // zero = no expiry
AuditChain []AuditEvent
// OnchainTx is the optional Lux EVM tx hash where the final envelope
// hash was anchored. Set only on completed envelopes that opted into
// on-chain notarization.
OnchainTx string
}
// Errors surfaced from the state machine and signature collection.
var (
ErrInvalidTransition = errors.New("envelope: invalid state transition")
ErrAlreadyTerminal = errors.New("envelope: already in terminal state")
ErrUnknownSigner = errors.New("envelope: signer not in slot list")
ErrSignerOutOfOrder = errors.New("envelope: signer signed out of declared order")
ErrDuplicateSignature = errors.New("envelope: signer already signed")
ErrDocHashMismatch = errors.New("envelope: signature dochash differs from envelope dochash")
)