From 76f7936ca6f67eee52c81b87b63fbfaac3fa958c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:48:51 +0200 Subject: [PATCH 01/88] define protocol security domains and trust boundaries --- docs/protocol-security.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/protocol-security.md diff --git a/docs/protocol-security.md b/docs/protocol-security.md new file mode 100644 index 00000000..a5deba8d --- /dev/null +++ b/docs/protocol-security.md @@ -0,0 +1,38 @@ +# Zephyr Protocol Security Model + +This document defines the security decisions for the protocol-hardening work tracked in issues #2, #3, #4, and #5. + +## Network identity + +The default development network identifier is `zephyr-devnet-1`. Production and public test networks must use distinct stable chain IDs. Nodes reject security-critical messages whose chain ID differs from their configured chain ID. + +## Versioned signing domains + +Every signed object is domain separated. The first protocol version uses: + +- `zephyr/transaction/v1` +- `zephyr/consensus/proposal/v1` +- `zephyr/consensus/vote/v1` +- `zephyr/transport/identity/v1` +- `zephyr/transport/request/v1` +- `zephyr/snapshot/v1` + +The domain and chain ID are part of the canonical payload. A signature from one message type or chain is invalid in another domain or chain. + +## Canonical P-256 signatures + +Zephyr uses raw 64-byte P-256 ECDSA signatures encoded as base64 (`r || s`). Signers normalize `s` to low-S and validators reject high-S signatures. Transaction IDs do not include signature bytes; they hash canonical transaction identity plus the public key, so equivalent ECDSA representations cannot create distinct transaction identities. + +## Request-bound peer authentication + +Peer authorization proofs bind the validator identity to the exact HTTP method, canonical request path, SHA-256 request-body hash, chain ID, nonce, and timestamp. Nodes reject reused nonces inside the accepted replay window. Replay state is persisted with bounded expiry so a restart does not reopen the replay window. + +Signed status identity remains a liveness/discovery proof and is not sufficient to authorize a state-changing peer request. + +## Snapshot trust + +A snapshot is accepted only when it is bound to the local chain ID, height, latest block hash, validator-set version, and a deterministic state commitment. The snapshot commitment covers consensus-critical ledger state and excludes local-only diagnostics/telemetry. + +Restore validation must fail closed before replacing known-good state. At minimum it validates block continuity and hashes, transaction IDs and signatures for the local chain, account/nonces/balance invariants, mempool invariants, validator voting-power arithmetic, and consensus certificate/proposal/vote consistency. + +A signed snapshot from one peer proves provenance, not correctness; state commitment and invariant validation are required independently of peer identity. From 8e214909ed55be075226b2c77b76cb521198b5c5 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:50:26 +0200 Subject: [PATCH 02/88] add canonical protocol security domains --- internal/protocol/security.go | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 internal/protocol/security.go diff --git a/internal/protocol/security.go b/internal/protocol/security.go new file mode 100644 index 00000000..932525e2 --- /dev/null +++ b/internal/protocol/security.go @@ -0,0 +1,42 @@ +package protocol + +import ( + "errors" + "strings" + "unicode" +) + +const ( + DefaultChainID = "zephyr-devnet-1" + + TransactionDomain = "zephyr/transaction/v1" + ConsensusProposalDomain = "zephyr/consensus/proposal/v1" + ConsensusVoteDomain = "zephyr/consensus/vote/v1" + TransportIdentityDomain = "zephyr/transport/identity/v1" + TransportRequestDomain = "zephyr/transport/request/v1" + SnapshotDomain = "zephyr/snapshot/v1" +) + +var ErrInvalidChainID = errors.New("invalid chain ID") + +func NormalizeChainID(chainID string) string { + chainID = strings.TrimSpace(chainID) + if chainID == "" { + return DefaultChainID + } + return chainID +} + +func ValidateChainID(chainID string) error { + chainID = strings.TrimSpace(chainID) + if chainID == "" || len(chainID) > 64 { + return ErrInvalidChainID + } + for _, r := range chainID { + if unicode.IsLetter(r) || unicode.IsDigit(r) || r == '-' || r == '_' || r == '.' { + continue + } + return ErrInvalidChainID + } + return nil +} From cc9e0779f0bf3ed41aad316c81f27e1912268a4b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:50:52 +0200 Subject: [PATCH 03/88] bind transactions to chain domains and canonical identity --- internal/tx/envelope.go | 189 +++++++++++++++++++++++++++++++--------- 1 file changed, 147 insertions(+), 42 deletions(-) diff --git a/internal/tx/envelope.go b/internal/tx/envelope.go index d6c78856..988a2a44 100644 --- a/internal/tx/envelope.go +++ b/internal/tx/envelope.go @@ -3,6 +3,7 @@ package tx import ( "crypto/ecdsa" "crypto/elliptic" + "crypto/rand" "crypto/sha256" "crypto/x509" "encoding/base64" @@ -10,6 +11,9 @@ import ( "encoding/json" "errors" "math/big" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/protocol" ) var ( @@ -19,9 +23,13 @@ var ( ErrInvalidPublicKey = errors.New("invalid public key") ErrInvalidAddress = errors.New("from address does not match public key") ErrInvalidSignature = errors.New("invalid signature") + ErrInvalidChainID = errors.New("transaction chain ID does not match local chain") + ErrInvalidDomain = errors.New("invalid transaction signing domain") ) type Envelope struct { + ChainID string `json:"chainId,omitempty"` + Domain string `json:"domain,omitempty"` From string `json:"from"` To string `json:"to"` Amount uint64 `json:"amount"` @@ -33,30 +41,72 @@ type Envelope struct { } type canonicalPayload struct { - Amount uint64 `json:"amount"` - From string `json:"from"` - Memo string `json:"memo"` - Nonce uint64 `json:"nonce"` - To string `json:"to"` + Amount uint64 `json:"amount"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` + From string `json:"from"` + Memo string `json:"memo"` + Nonce uint64 `json:"nonce"` + To string `json:"to"` +} + +type canonicalIdentity struct { + Amount uint64 `json:"amount"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` + From string `json:"from"` + Memo string `json:"memo"` + Nonce uint64 `json:"nonce"` + PublicKey string `json:"publicKey"` + To string `json:"to"` +} + +func (e Envelope) EffectiveChainID() string { + return protocol.NormalizeChainID(e.ChainID) +} + +func (e Envelope) EffectiveDomain() string { + if strings.TrimSpace(e.Domain) == "" { + return protocol.TransactionDomain + } + return strings.TrimSpace(e.Domain) } func (e Envelope) CanonicalPayload() string { + return e.CanonicalPayloadForChain(e.EffectiveChainID()) +} + +func (e Envelope) CanonicalPayloadForChain(chainID string) string { payload, _ := json.Marshal(canonicalPayload{ - Amount: e.Amount, - From: e.From, - Memo: e.Memo, - Nonce: e.Nonce, - To: e.To, + Amount: e.Amount, + ChainID: protocol.NormalizeChainID(chainID), + Domain: e.EffectiveDomain(), + From: e.From, + Memo: e.Memo, + Nonce: e.Nonce, + To: e.To, }) - return string(payload) } func (e Envelope) ValidateStatic() error { + return e.ValidateForChain(protocol.DefaultChainID) +} + +func (e Envelope) ValidateForChain(expectedChainID string) error { + expectedChainID = protocol.NormalizeChainID(expectedChainID) + if err := protocol.ValidateChainID(expectedChainID); err != nil { + return ErrInvalidChainID + } + if e.EffectiveChainID() != expectedChainID { + return ErrInvalidChainID + } + if e.EffectiveDomain() != protocol.TransactionDomain { + return ErrInvalidDomain + } if e.From == "" || e.To == "" || e.Payload == "" || e.PublicKey == "" || e.Signature == "" { return ErrMissingFields } - if e.Amount == 0 { return ErrInvalidAmount } @@ -65,15 +115,12 @@ func (e Envelope) ValidateStatic() error { if err != nil { return err } - if address != e.From { return ErrInvalidAddress } - - if e.Payload != e.CanonicalPayload() { + if e.Payload != e.CanonicalPayloadForChain(expectedChainID) { return ErrInvalidPayload } - return VerifySignature(e.PublicKey, e.Payload, e.Signature) } @@ -82,64 +129,122 @@ func DeriveAddressFromPublicKey(encodedPublicKey string) (string, error) { if err != nil { return "", ErrInvalidPublicKey } + parsedPublicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes) + if err != nil { + return "", ErrInvalidPublicKey + } + publicKey, ok := parsedPublicKey.(*ecdsa.PublicKey) + if !ok || publicKey.Curve.Params().Name != elliptic.P256().Params().Name { + return "", ErrInvalidPublicKey + } sum := sha256.Sum256(publicKeyBytes) return "zph_" + hex.EncodeToString(sum[:])[:40], nil } +func SignPayload(privateKey *ecdsa.PrivateKey, payload string) (string, error) { + if privateKey == nil || privateKey.Curve == nil || privateKey.Curve.Params().Name != elliptic.P256().Params().Name { + return "", ErrInvalidPublicKey + } + digest := sha256.Sum256([]byte(payload)) + r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:]) + if err != nil { + return "", err + } + s = normalizeLowS(s) + signature := append(pad32(r), pad32(s)...) + return base64.StdEncoding.EncodeToString(signature), nil +} + +func NormalizeP256Signature(encodedSignature string) (string, error) { + r, s, err := decodeSignature(encodedSignature) + if err != nil { + return "", err + } + s = normalizeLowS(s) + signature := append(pad32(r), pad32(s)...) + return base64.StdEncoding.EncodeToString(signature), nil +} + +func IsCanonicalP256Signature(encodedSignature string) bool { + _, s, err := decodeSignature(encodedSignature) + if err != nil { + return false + } + return s.Cmp(halfOrder()) <= 0 +} + func VerifySignature(encodedPublicKey string, payload string, encodedSignature string) error { publicKeyBytes, err := base64.StdEncoding.DecodeString(encodedPublicKey) if err != nil { return ErrInvalidPublicKey } - parsedPublicKey, err := x509.ParsePKIXPublicKey(publicKeyBytes) if err != nil { return ErrInvalidPublicKey } - publicKey, ok := parsedPublicKey.(*ecdsa.PublicKey) if !ok || publicKey.Curve.Params().Name != elliptic.P256().Params().Name { return ErrInvalidPublicKey } - signatureBytes, err := base64.StdEncoding.DecodeString(encodedSignature) - if err != nil || len(signatureBytes) != 64 { - return ErrInvalidSignature + r, s, err := decodeSignature(encodedSignature) + if err != nil { + return err } - - r := new(big.Int).SetBytes(signatureBytes[:32]) - s := new(big.Int).SetBytes(signatureBytes[32:]) digest := sha256.Sum256([]byte(payload)) - if !ecdsa.Verify(publicKey, digest[:], r, s) { return ErrInvalidSignature } - return nil } func ID(e Envelope) string { - payload, _ := json.Marshal(struct { - From string `json:"from"` - To string `json:"to"` - Amount uint64 `json:"amount"` - Nonce uint64 `json:"nonce"` - Memo string `json:"memo"` - Payload string `json:"payload"` - PublicKey string `json:"publicKey"` - Signature string `json:"signature"` - }{ - From: e.From, - To: e.To, + payload, _ := json.Marshal(canonicalIdentity{ Amount: e.Amount, - Nonce: e.Nonce, + ChainID: e.EffectiveChainID(), + Domain: e.EffectiveDomain(), + From: e.From, Memo: e.Memo, - Payload: e.Payload, + Nonce: e.Nonce, PublicKey: e.PublicKey, - Signature: e.Signature, + To: e.To, }) - sum := sha256.Sum256(payload) return hex.EncodeToString(sum[:]) } + +func decodeSignature(encodedSignature string) (*big.Int, *big.Int, error) { + signatureBytes, err := base64.StdEncoding.DecodeString(encodedSignature) + if err != nil || len(signatureBytes) != 64 { + return nil, nil, ErrInvalidSignature + } + r := new(big.Int).SetBytes(signatureBytes[:32]) + s := new(big.Int).SetBytes(signatureBytes[32:]) + order := elliptic.P256().Params().N + if r.Sign() <= 0 || s.Sign() <= 0 || r.Cmp(order) >= 0 || s.Cmp(order) >= 0 { + return nil, nil, ErrInvalidSignature + } + return r, s, nil +} + +func normalizeLowS(s *big.Int) *big.Int { + if s.Cmp(halfOrder()) <= 0 { + return new(big.Int).Set(s) + } + return new(big.Int).Sub(elliptic.P256().Params().N, s) +} + +func halfOrder() *big.Int { + return new(big.Int).Rsh(new(big.Int).Set(elliptic.P256().Params().N), 1) +} + +func pad32(value *big.Int) []byte { + bytes := value.Bytes() + if len(bytes) >= 32 { + return bytes[len(bytes)-32:] + } + padded := make([]byte, 32) + copy(padded[32-len(bytes):], bytes) + return padded +} From c9064bede6b3c34829378ddd1e04710e28cac326 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:54:08 +0200 Subject: [PATCH 04/88] make protocol chain identity explicit --- internal/protocol/security.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/protocol/security.go b/internal/protocol/security.go index 932525e2..b74c3f81 100644 --- a/internal/protocol/security.go +++ b/internal/protocol/security.go @@ -19,7 +19,7 @@ const ( var ErrInvalidChainID = errors.New("invalid chain ID") -func NormalizeChainID(chainID string) string { +func ConfiguredChainID(chainID string) string { chainID = strings.TrimSpace(chainID) if chainID == "" { return DefaultChainID From dc8c417f9a656c4761664eabbbec38a525b2a745 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:54:37 +0200 Subject: [PATCH 05/88] require explicit transaction chain and canonical signatures --- internal/tx/envelope.go | 78 +++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 42 deletions(-) diff --git a/internal/tx/envelope.go b/internal/tx/envelope.go index 988a2a44..c01f68c8 100644 --- a/internal/tx/envelope.go +++ b/internal/tx/envelope.go @@ -17,19 +17,20 @@ import ( ) var ( - ErrMissingFields = errors.New("missing required transaction fields") - ErrInvalidAmount = errors.New("amount must be greater than zero") - ErrInvalidPayload = errors.New("payload does not match canonical transaction") - ErrInvalidPublicKey = errors.New("invalid public key") - ErrInvalidAddress = errors.New("from address does not match public key") - ErrInvalidSignature = errors.New("invalid signature") - ErrInvalidChainID = errors.New("transaction chain ID does not match local chain") - ErrInvalidDomain = errors.New("invalid transaction signing domain") + ErrMissingFields = errors.New("missing required transaction fields") + ErrInvalidAmount = errors.New("amount must be greater than zero") + ErrInvalidPayload = errors.New("payload does not match canonical transaction") + ErrInvalidPublicKey = errors.New("invalid public key") + ErrInvalidAddress = errors.New("from address does not match public key") + ErrInvalidSignature = errors.New("invalid signature") + ErrNonCanonicalSignature = errors.New("signature must use canonical low-S P-256 form") + ErrInvalidChainID = errors.New("transaction chain ID does not match local chain") + ErrInvalidDomain = errors.New("invalid transaction signing domain") ) type Envelope struct { - ChainID string `json:"chainId,omitempty"` - Domain string `json:"domain,omitempty"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` From string `json:"from"` To string `json:"to"` Amount uint64 `json:"amount"` @@ -61,26 +62,11 @@ type canonicalIdentity struct { To string `json:"to"` } -func (e Envelope) EffectiveChainID() string { - return protocol.NormalizeChainID(e.ChainID) -} - -func (e Envelope) EffectiveDomain() string { - if strings.TrimSpace(e.Domain) == "" { - return protocol.TransactionDomain - } - return strings.TrimSpace(e.Domain) -} - func (e Envelope) CanonicalPayload() string { - return e.CanonicalPayloadForChain(e.EffectiveChainID()) -} - -func (e Envelope) CanonicalPayloadForChain(chainID string) string { payload, _ := json.Marshal(canonicalPayload{ Amount: e.Amount, - ChainID: protocol.NormalizeChainID(chainID), - Domain: e.EffectiveDomain(), + ChainID: strings.TrimSpace(e.ChainID), + Domain: strings.TrimSpace(e.Domain), From: e.From, Memo: e.Memo, Nonce: e.Nonce, @@ -90,23 +76,17 @@ func (e Envelope) CanonicalPayloadForChain(chainID string) string { } func (e Envelope) ValidateStatic() error { - return e.ValidateForChain(protocol.DefaultChainID) -} - -func (e Envelope) ValidateForChain(expectedChainID string) error { - expectedChainID = protocol.NormalizeChainID(expectedChainID) - if err := protocol.ValidateChainID(expectedChainID); err != nil { - return ErrInvalidChainID + chainID := strings.TrimSpace(e.ChainID) + domain := strings.TrimSpace(e.Domain) + if chainID == "" || domain == "" || e.From == "" || e.To == "" || e.Payload == "" || e.PublicKey == "" || e.Signature == "" { + return ErrMissingFields } - if e.EffectiveChainID() != expectedChainID { + if err := protocol.ValidateChainID(chainID); err != nil { return ErrInvalidChainID } - if e.EffectiveDomain() != protocol.TransactionDomain { + if domain != protocol.TransactionDomain { return ErrInvalidDomain } - if e.From == "" || e.To == "" || e.Payload == "" || e.PublicKey == "" || e.Signature == "" { - return ErrMissingFields - } if e.Amount == 0 { return ErrInvalidAmount } @@ -118,12 +98,23 @@ func (e Envelope) ValidateForChain(expectedChainID string) error { if address != e.From { return ErrInvalidAddress } - if e.Payload != e.CanonicalPayloadForChain(expectedChainID) { + if e.Payload != e.CanonicalPayload() { return ErrInvalidPayload } return VerifySignature(e.PublicKey, e.Payload, e.Signature) } +func (e Envelope) ValidateForChain(expectedChainID string) error { + if err := e.ValidateStatic(); err != nil { + return err + } + expectedChainID = strings.TrimSpace(expectedChainID) + if err := protocol.ValidateChainID(expectedChainID); err != nil || strings.TrimSpace(e.ChainID) != expectedChainID { + return ErrInvalidChainID + } + return nil +} + func DeriveAddressFromPublicKey(encodedPublicKey string) (string, error) { publicKeyBytes, err := base64.StdEncoding.DecodeString(encodedPublicKey) if err != nil { @@ -192,6 +183,9 @@ func VerifySignature(encodedPublicKey string, payload string, encodedSignature s if err != nil { return err } + if s.Cmp(halfOrder()) > 0 { + return ErrNonCanonicalSignature + } digest := sha256.Sum256([]byte(payload)) if !ecdsa.Verify(publicKey, digest[:], r, s) { return ErrInvalidSignature @@ -202,8 +196,8 @@ func VerifySignature(encodedPublicKey string, payload string, encodedSignature s func ID(e Envelope) string { payload, _ := json.Marshal(canonicalIdentity{ Amount: e.Amount, - ChainID: e.EffectiveChainID(), - Domain: e.EffectiveDomain(), + ChainID: strings.TrimSpace(e.ChainID), + Domain: strings.TrimSpace(e.Domain), From: e.From, Memo: e.Memo, Nonce: e.Nonce, From f5d091876aa3671519611cfb9fbe4917c61e7a62 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:55:12 +0200 Subject: [PATCH 06/88] require chain-bound consensus signing domains --- internal/consensus/messages.go | 75 +++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/internal/consensus/messages.go b/internal/consensus/messages.go index e701ce21..11358989 100644 --- a/internal/consensus/messages.go +++ b/internal/consensus/messages.go @@ -8,6 +8,7 @@ import ( "strings" "time" + "github.com/zephyr-chain/zephyr-chain/internal/protocol" "github.com/zephyr-chain/zephyr-chain/internal/tx" ) @@ -17,6 +18,8 @@ var ( ErrInvalidPublicKey = errors.New("invalid public key") ErrInvalidAddress = errors.New("signer address does not match public key") ErrInvalidSignature = errors.New("invalid signature") + ErrInvalidChainID = errors.New("consensus message chain ID does not match local chain") + ErrInvalidDomain = errors.New("invalid consensus signing domain") ErrInvalidHash = errors.New("invalid block hash") ErrInvalidHeight = errors.New("height must be greater than zero") ErrInvalidProducedAt = errors.New("producedAt must be set") @@ -28,6 +31,8 @@ var ( ) type Proposal struct { + ChainID string `json:"chainId"` + Domain string `json:"domain"` Height uint64 `json:"height"` Round uint64 `json:"round"` BlockHash string `json:"blockHash"` @@ -43,6 +48,8 @@ type Proposal struct { } type Vote struct { + ChainID string `json:"chainId"` + Domain string `json:"domain"` Height uint64 `json:"height"` Round uint64 `json:"round"` BlockHash string `json:"blockHash"` @@ -55,6 +62,8 @@ type Vote struct { type canonicalProposal struct { BlockHash string `json:"blockHash"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` Height uint64 `json:"height"` PreviousHash string `json:"previousHash"` ProducedAt string `json:"producedAt"` @@ -65,6 +74,8 @@ type canonicalProposal struct { type canonicalVote struct { BlockHash string `json:"blockHash"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` Height uint64 `json:"height"` Round uint64 `json:"round"` Voter string `json:"voter"` @@ -90,6 +101,8 @@ func BlockHash(height uint64, previousHash string, producedAt time.Time, transac func (p Proposal) CanonicalPayload() string { payload, _ := json.Marshal(canonicalProposal{ BlockHash: p.BlockHash, + ChainID: strings.TrimSpace(p.ChainID), + Domain: strings.TrimSpace(p.Domain), Height: p.Height, PreviousHash: p.PreviousHash, ProducedAt: p.ProducedAt.UTC().Format(time.RFC3339Nano), @@ -106,12 +119,20 @@ func (p Proposal) CandidateHash() string { } func (p Proposal) ValidateStatic() error { + chainID := strings.TrimSpace(p.ChainID) + domain := strings.TrimSpace(p.Domain) + if chainID == "" || domain == "" || p.BlockHash == "" || p.Proposer == "" || p.Payload == "" || p.PublicKey == "" || p.Signature == "" || len(p.TransactionIDs) == 0 { + return ErrMissingFields + } + if err := protocol.ValidateChainID(chainID); err != nil { + return ErrInvalidChainID + } + if domain != protocol.ConsensusProposalDomain { + return ErrInvalidDomain + } if p.Height == 0 { return ErrInvalidHeight } - if p.BlockHash == "" || p.Proposer == "" || p.Payload == "" || p.PublicKey == "" || p.Signature == "" || len(p.TransactionIDs) == 0 { - return ErrMissingFields - } if p.ProducedAt.IsZero() { return ErrInvalidProducedAt } @@ -124,7 +145,7 @@ func (p Proposal) ValidateStatic() error { if err := validateTransactionIDs(p.TransactionIDs); err != nil { return err } - if err := validateProposalTransactions(p.TransactionIDs, p.Transactions); err != nil { + if err := validateProposalTransactions(chainID, p.TransactionIDs, p.Transactions); err != nil { return err } if p.BlockHash != p.CandidateHash() { @@ -148,7 +169,7 @@ func (p Proposal) ValidateStatic() error { switch { case errors.Is(err, tx.ErrInvalidPublicKey): return ErrInvalidPublicKey - case errors.Is(err, tx.ErrInvalidSignature): + case errors.Is(err, tx.ErrInvalidSignature), errors.Is(err, tx.ErrNonCanonicalSignature): return ErrInvalidSignature default: return err @@ -158,9 +179,22 @@ func (p Proposal) ValidateStatic() error { return nil } +func (p Proposal) ValidateForChain(expectedChainID string) error { + if err := p.ValidateStatic(); err != nil { + return err + } + expectedChainID = strings.TrimSpace(expectedChainID) + if err := protocol.ValidateChainID(expectedChainID); err != nil || strings.TrimSpace(p.ChainID) != expectedChainID { + return ErrInvalidChainID + } + return nil +} + func (v Vote) CanonicalPayload() string { payload, _ := json.Marshal(canonicalVote{ BlockHash: v.BlockHash, + ChainID: strings.TrimSpace(v.ChainID), + Domain: strings.TrimSpace(v.Domain), Height: v.Height, Round: v.Round, Voter: v.Voter, @@ -170,12 +204,20 @@ func (v Vote) CanonicalPayload() string { } func (v Vote) ValidateStatic() error { + chainID := strings.TrimSpace(v.ChainID) + domain := strings.TrimSpace(v.Domain) + if chainID == "" || domain == "" || v.BlockHash == "" || v.Voter == "" || v.Payload == "" || v.PublicKey == "" || v.Signature == "" { + return ErrMissingFields + } + if err := protocol.ValidateChainID(chainID); err != nil { + return ErrInvalidChainID + } + if domain != protocol.ConsensusVoteDomain { + return ErrInvalidDomain + } if v.Height == 0 { return ErrInvalidHeight } - if v.BlockHash == "" || v.Voter == "" || v.Payload == "" || v.PublicKey == "" || v.Signature == "" { - return ErrMissingFields - } if err := validateHash(v.BlockHash, false); err != nil { return err } @@ -197,7 +239,7 @@ func (v Vote) ValidateStatic() error { switch { case errors.Is(err, tx.ErrInvalidPublicKey): return ErrInvalidPublicKey - case errors.Is(err, tx.ErrInvalidSignature): + case errors.Is(err, tx.ErrInvalidSignature), errors.Is(err, tx.ErrNonCanonicalSignature): return ErrInvalidSignature default: return err @@ -207,6 +249,17 @@ func (v Vote) ValidateStatic() error { return nil } +func (v Vote) ValidateForChain(expectedChainID string) error { + if err := v.ValidateStatic(); err != nil { + return err + } + expectedChainID = strings.TrimSpace(expectedChainID) + if err := protocol.ValidateChainID(expectedChainID); err != nil || strings.TrimSpace(v.ChainID) != expectedChainID { + return ErrInvalidChainID + } + return nil +} + func validateHash(value string, allowEmpty bool) error { value = strings.TrimSpace(value) if value == "" { @@ -238,7 +291,7 @@ func validateTransactionIDs(transactionIDs []string) error { return nil } -func validateProposalTransactions(transactionIDs []string, transactions []tx.Envelope) error { +func validateProposalTransactions(chainID string, transactionIDs []string, transactions []tx.Envelope) error { if len(transactions) == 0 { return ErrMissingTransactions } @@ -246,7 +299,7 @@ func validateProposalTransactions(transactionIDs []string, transactions []tx.Env return ErrTransactionMismatch } for index, envelope := range transactions { - if err := envelope.ValidateStatic(); err != nil { + if err := envelope.ValidateForChain(chainID); err != nil { return ErrInvalidProposalTransaction } if tx.ID(envelope) != transactionIDs[index] { From 3b72257f5d006bf76d31c57e2054749896af74f0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:56:35 +0200 Subject: [PATCH 07/88] add request-bound replay-safe peer authentication --- internal/api/request_auth.go | 347 +++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) create mode 100644 internal/api/request_auth.go diff --git a/internal/api/request_auth.go b/internal/api/request_auth.go new file mode 100644 index 00000000..d66017f0 --- /dev/null +++ b/internal/api/request_auth.go @@ -0,0 +1,347 @@ +package api + +import ( + "bytes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + "github.com/zephyr-chain/zephyr-chain/internal/protocol" + "github.com/zephyr-chain/zephyr-chain/internal/tx" +) + +const ( + sourceChainIDHeader = "X-Zephyr-Chain-ID" + sourceRequestDomainHeader = "X-Zephyr-Request-Domain" + sourceRequestNonceHeader = "X-Zephyr-Request-Nonce" + maxReplayEntries = 10000 +) + +var ( + errMissingRequestProof = errors.New("peer request must include a signed request proof") + errInvalidRequestProof = errors.New("invalid peer request proof") + errRequestChainMismatch = errors.New("peer request chain ID does not match local chain") + errRequestDomainMismatch = errors.New("invalid peer request signing domain") + errRequestTimestamp = errors.New("peer request timestamp is outside the allowed window") + errRequestReplay = errors.New("peer request nonce has already been used") + errRequestReplayStoreFull = errors.New("peer request replay store is full") +) + +type requestProof struct { + BodyHash string + ChainID string + Domain string + Method string + NodeID string + Nonce string + Path string + Payload string + PublicKey string + Signature string + SignedAt time.Time + ValidatorAddress string +} + +type canonicalRequestProof struct { + BodyHash string `json:"bodyHash"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` + Method string `json:"method"` + NodeID string `json:"nodeId"` + Nonce string `json:"nonce"` + Path string `json:"path"` + SignedAt string `json:"signedAt"` + ValidatorAddress string `json:"validatorAddress"` +} + +func (p requestProof) canonicalPayload() string { + payload, _ := json.Marshal(canonicalRequestProof{ + BodyHash: p.BodyHash, + ChainID: strings.TrimSpace(p.ChainID), + Domain: strings.TrimSpace(p.Domain), + Method: strings.ToUpper(strings.TrimSpace(p.Method)), + NodeID: strings.TrimSpace(p.NodeID), + Nonce: strings.TrimSpace(p.Nonce), + Path: p.Path, + SignedAt: p.SignedAt.UTC().Format(time.RFC3339Nano), + ValidatorAddress: strings.TrimSpace(p.ValidatorAddress), + }) + return string(payload) +} + +func (s *transportIdentitySigner) buildRequestProof(method string, path string, body []byte, now time.Time) (requestProof, error) { + if s == nil { + return requestProof{}, errInvalidValidatorPrivateKey + } + if now.IsZero() { + now = time.Now().UTC() + } + nonceBytes := make([]byte, 16) + if _, err := rand.Read(nonceBytes); err != nil { + return requestProof{}, err + } + proof := requestProof{ + BodyHash: bodySHA256(body), + ChainID: s.chainID, + Domain: protocol.TransportRequestDomain, + Method: strings.ToUpper(method), + NodeID: s.nodeID, + Nonce: hex.EncodeToString(nonceBytes), + Path: path, + PublicKey: s.publicKey, + SignedAt: now.UTC(), + ValidatorAddress: s.validatorAddress, + } + proof.Payload = proof.canonicalPayload() + signature, err := tx.SignPayload(s.privateKey, proof.Payload) + if err != nil { + return requestProof{}, err + } + proof.Signature = signature + return proof, nil +} + +func requestProofFromRequest(r *http.Request, expectedChainID string) (*requestProof, []byte, error) { + if strings.TrimSpace(r.Header.Get(sourceNodeHeader)) == "" { + return nil, nil, nil + } + + body, err := readAndRestoreRequestBody(r) + if err != nil { + return nil, nil, errInvalidRequestProof + } + proof := &requestProof{ + BodyHash: bodySHA256(body), + ChainID: strings.TrimSpace(r.Header.Get(sourceChainIDHeader)), + Domain: strings.TrimSpace(r.Header.Get(sourceRequestDomainHeader)), + Method: r.Method, + NodeID: strings.TrimSpace(r.Header.Get(sourceNodeHeader)), + Nonce: strings.TrimSpace(r.Header.Get(sourceRequestNonceHeader)), + Path: canonicalRequestPath(r), + Payload: strings.TrimSpace(r.Header.Get(sourceIdentityPayloadHeader)), + PublicKey: strings.TrimSpace(r.Header.Get(sourcePublicKeyHeader)), + Signature: strings.TrimSpace(r.Header.Get(sourceSignatureHeader)), + ValidatorAddress: strings.TrimSpace(r.Header.Get(sourceValidatorHeader)), + } + signedAtRaw := strings.TrimSpace(r.Header.Get(sourceSignedAtHeader)) + if proof.ChainID == "" || proof.Domain == "" || proof.NodeID == "" || proof.Nonce == "" || proof.Payload == "" || proof.PublicKey == "" || proof.Signature == "" || proof.ValidatorAddress == "" || signedAtRaw == "" { + return nil, body, errMissingRequestProof + } + proof.SignedAt, err = time.Parse(time.RFC3339Nano, signedAtRaw) + if err != nil { + return nil, body, errRequestTimestamp + } + proof.SignedAt = proof.SignedAt.UTC() + + expectedChainID = strings.TrimSpace(expectedChainID) + if proof.ChainID != expectedChainID { + return nil, body, errRequestChainMismatch + } + if proof.Domain != protocol.TransportRequestDomain { + return nil, body, errRequestDomainMismatch + } + if proof.Payload != proof.canonicalPayload() { + return nil, body, errInvalidRequestProof + } + address, err := tx.DeriveAddressFromPublicKey(proof.PublicKey) + if err != nil || address != proof.ValidatorAddress { + return nil, body, errInvalidRequestProof + } + now := time.Now().UTC() + if proof.SignedAt.Before(now.Add(-transportIdentityMaxSkew)) || proof.SignedAt.After(now.Add(transportIdentityMaxSkew)) { + return nil, body, errRequestTimestamp + } + if err := tx.VerifySignature(proof.PublicKey, proof.Payload, proof.Signature); err != nil { + return nil, body, errInvalidRequestProof + } + return proof, body, nil +} + +func canonicalRequestPath(r *http.Request) string { + if r == nil || r.URL == nil { + return "" + } + path := r.URL.EscapedPath() + if path == "" { + path = "/" + } + if r.URL.RawQuery != "" { + path += "?" + r.URL.RawQuery + } + return path +} + +func readAndRestoreRequestBody(r *http.Request) ([]byte, error) { + if r.Body == nil { + return nil, nil + } + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, err + } + r.Body = io.NopCloser(bytes.NewReader(body)) + return body, nil +} + +func bodySHA256(body []byte) string { + digest := sha256.Sum256(body) + return hex.EncodeToString(digest[:]) +} + +type replayState struct { + Entries []replayEntry `json:"entries"` +} + +type replayEntry struct { + Key string `json:"key"` + ExpiresAt time.Time `json:"expiresAt"` +} + +type requestReplayGuard struct { + mu sync.Mutex + path string + entries map[string]time.Time +} + +var replayGuards = struct { + sync.Mutex + byServer map[*Server]*requestReplayGuard +}{byServer: make(map[*Server]*requestReplayGuard)} + +func replayGuardForServer(s *Server) (*requestReplayGuard, error) { + replayGuards.Lock() + defer replayGuards.Unlock() + if guard := replayGuards.byServer[s]; guard != nil { + return guard, nil + } + guard, err := newRequestReplayGuard(filepath.Join(s.ledger.DataDir(), "request-replay.json")) + if err != nil { + return nil, err + } + replayGuards.byServer[s] = guard + return guard, nil +} + +func newRequestReplayGuard(path string) (*requestReplayGuard, error) { + guard := &requestReplayGuard{path: path, entries: make(map[string]time.Time)} + raw, err := os.ReadFile(path) + if errors.Is(err, os.ErrNotExist) { + return guard, nil + } + if err != nil { + return nil, err + } + if len(raw) == 0 { + return guard, nil + } + var state replayState + if err := json.Unmarshal(raw, &state); err != nil { + return nil, err + } + now := time.Now().UTC() + for _, entry := range state.Entries { + if entry.Key == "" || !entry.ExpiresAt.After(now) { + continue + } + guard.entries[entry.Key] = entry.ExpiresAt.UTC() + } + return guard, nil +} + +func (g *requestReplayGuard) remember(proof *requestProof) error { + if g == nil || proof == nil { + return errInvalidRequestProof + } + g.mu.Lock() + defer g.mu.Unlock() + + now := time.Now().UTC() + for key, expiresAt := range g.entries { + if !expiresAt.After(now) { + delete(g.entries, key) + } + } + key := proof.ValidatorAddress + "|" + proof.Nonce + if expiresAt, exists := g.entries[key]; exists && expiresAt.After(now) { + return errRequestReplay + } + if len(g.entries) >= maxReplayEntries { + return errRequestReplayStoreFull + } + g.entries[key] = proof.SignedAt.Add(transportIdentityMaxSkew).UTC() + return g.persistLocked() +} + +func (g *requestReplayGuard) persistLocked() error { + entries := make([]replayEntry, 0, len(g.entries)) + for key, expiresAt := range g.entries { + entries = append(entries, replayEntry{Key: key, ExpiresAt: expiresAt}) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Key < entries[j].Key }) + raw, err := json.MarshalIndent(replayState{Entries: entries}, "", " ") + if err != nil { + return err + } + return writeSecurityStateAtomic(g.path, raw) +} + +func writeSecurityStateAtomic(path string, raw []byte) error { + dir := filepath.Dir(path) + if err := os.MkdirAll(dir, 0o700); err != nil { + return err + } + temp, err := os.CreateTemp(dir, ".zephyr-security-*") + if err != nil { + return err + } + tempPath := temp.Name() + defer os.Remove(tempPath) + if err := temp.Chmod(0o600); err != nil { + temp.Close() + return err + } + if _, err := temp.Write(raw); err != nil { + temp.Close() + return err + } + if err := temp.Sync(); err != nil { + temp.Close() + return err + } + if err := temp.Close(); err != nil { + return err + } + if err := os.Rename(tempPath, path); err != nil { + return err + } + return os.Chmod(path, 0o600) +} + +func validateAndRememberRequestProof(s *Server, r *http.Request) (*requestProof, error) { + proof, _, err := requestProofFromRequest(r, s.config.ChainID) + if err != nil { + return nil, err + } + if proof == nil { + return nil, nil + } + guard, err := replayGuardForServer(s) + if err != nil { + return nil, fmt.Errorf("request replay guard: %w", err) + } + if err := guard.remember(proof); err != nil { + return nil, err + } + return proof, nil +} From cf00703553f074600d85d5c90e5e2f99ef222704 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:57:15 +0200 Subject: [PATCH 08/88] bind validator identities and consensus signing to chain domains --- internal/api/transport_identity.go | 67 ++++++++++++++++-------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/internal/api/transport_identity.go b/internal/api/transport_identity.go index 7d88f57c..c23bfcf2 100644 --- a/internal/api/transport_identity.go +++ b/internal/api/transport_identity.go @@ -3,19 +3,17 @@ package api import ( "crypto/ecdsa" "crypto/elliptic" - "crypto/rand" - "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "errors" - "math/big" "net/http" "strings" "time" "github.com/zephyr-chain/zephyr-chain/internal/consensus" + "github.com/zephyr-chain/zephyr-chain/internal/protocol" "github.com/zephyr-chain/zephyr-chain/internal/tx" ) @@ -28,6 +26,7 @@ var ( errInvalidTransportIdentitySignature = errors.New("invalid transport identity signature") errTransportIdentityNodeMismatch = errors.New("transport identity node does not match status response") errTransportIdentityValidatorMismatch = errors.New("transport identity validator does not match status response") + errTransportIdentityChainMismatch = errors.New("transport identity chain does not match status response") errInvalidValidatorPrivateKey = errors.New("invalid validator private key") errValidatorIdentityMismatch = errors.New("validator private key does not match configured validator address") ) @@ -35,6 +34,8 @@ var ( const transportIdentityMaxSkew = 2 * time.Minute type TransportIdentity struct { + ChainID string `json:"chainId"` + Domain string `json:"domain"` NodeID string `json:"nodeId"` ValidatorAddress string `json:"validatorAddress"` Payload string `json:"payload"` @@ -44,12 +45,15 @@ type TransportIdentity struct { } type canonicalTransportIdentity struct { + ChainID string `json:"chainId"` + Domain string `json:"domain"` NodeID string `json:"nodeId"` SignedAt string `json:"signedAt"` ValidatorAddress string `json:"validatorAddress"` } type transportIdentitySigner struct { + chainID string nodeID string validatorAddress string publicKey string @@ -58,6 +62,8 @@ type transportIdentitySigner struct { func (i TransportIdentity) CanonicalPayload() string { payload, _ := json.Marshal(canonicalTransportIdentity{ + ChainID: strings.TrimSpace(i.ChainID), + Domain: strings.TrimSpace(i.Domain), NodeID: i.NodeID, SignedAt: i.SignedAt.UTC().Format(time.RFC3339Nano), ValidatorAddress: i.ValidatorAddress, @@ -66,9 +72,15 @@ func (i TransportIdentity) CanonicalPayload() string { } func (i TransportIdentity) ValidateAt(now time.Time) error { - if i.NodeID == "" || i.ValidatorAddress == "" || i.Payload == "" || i.PublicKey == "" || i.Signature == "" { + if i.ChainID == "" || i.Domain == "" || i.NodeID == "" || i.ValidatorAddress == "" || i.Payload == "" || i.PublicKey == "" || i.Signature == "" { return errMissingTransportIdentityFields } + if err := protocol.ValidateChainID(strings.TrimSpace(i.ChainID)); err != nil { + return errTransportIdentityChainMismatch + } + if strings.TrimSpace(i.Domain) != protocol.TransportIdentityDomain { + return errInvalidTransportIdentityPayload + } if i.SignedAt.IsZero() { return errTransportIdentityTimestamp } @@ -94,7 +106,7 @@ func (i TransportIdentity) ValidateAt(now time.Time) error { switch { case errors.Is(err, tx.ErrInvalidPublicKey): return errInvalidTransportIdentityPublicKey - case errors.Is(err, tx.ErrInvalidSignature): + case errors.Is(err, tx.ErrInvalidSignature), errors.Is(err, tx.ErrNonCanonicalSignature): return errInvalidTransportIdentitySignature default: return err @@ -123,6 +135,7 @@ func newTransportIdentitySigner(config Config) (*transportIdentitySigner, Config config.ValidatorAddress = address return &transportIdentitySigner{ + chainID: config.ChainID, nodeID: config.NodeID, validatorAddress: address, publicKey: publicKey, @@ -138,13 +151,15 @@ func (s *transportIdentitySigner) Build(now time.Time) (TransportIdentity, error now = time.Now().UTC() } identity := TransportIdentity{ + ChainID: s.chainID, + Domain: protocol.TransportIdentityDomain, NodeID: s.nodeID, ValidatorAddress: s.validatorAddress, PublicKey: s.publicKey, SignedAt: now.UTC(), } identity.Payload = identity.CanonicalPayload() - signature, err := signTransportIdentityPayload(s.privateKey, identity.Payload) + signature, err := tx.SignPayload(s.privateKey, identity.Payload) if err != nil { return TransportIdentity{}, err } @@ -159,13 +174,15 @@ func (s *transportIdentitySigner) SignProposal(proposal consensus.Proposal, now if now.IsZero() { now = time.Now().UTC() } + proposal.ChainID = s.chainID + proposal.Domain = protocol.ConsensusProposalDomain proposal.Proposer = s.validatorAddress proposal.PublicKey = s.publicKey if proposal.ProposedAt.IsZero() { proposal.ProposedAt = now.UTC() } proposal.Payload = proposal.CanonicalPayload() - signature, err := signTransportIdentityPayload(s.privateKey, proposal.Payload) + signature, err := tx.SignPayload(s.privateKey, proposal.Payload) if err != nil { return consensus.Proposal{}, err } @@ -180,13 +197,15 @@ func (s *transportIdentitySigner) SignVote(vote consensus.Vote, now time.Time) ( if now.IsZero() { now = time.Now().UTC() } + vote.ChainID = s.chainID + vote.Domain = protocol.ConsensusVoteDomain vote.Voter = s.validatorAddress vote.PublicKey = s.publicKey if vote.VotedAt.IsZero() { vote.VotedAt = now.UTC() } vote.Payload = vote.CanonicalPayload() - signature, err := signTransportIdentityPayload(s.privateKey, vote.Payload) + signature, err := tx.SignPayload(s.privateKey, vote.Payload) if err != nil { return consensus.Vote{}, err } @@ -241,26 +260,6 @@ func parseECDSAPrivateKey(keyBytes []byte) (*ecdsa.PrivateKey, error) { return privateKey, nil } -func signTransportIdentityPayload(privateKey *ecdsa.PrivateKey, payload string) (string, error) { - digest := sha256.Sum256([]byte(payload)) - r, s, err := ecdsa.Sign(rand.Reader, privateKey, digest[:]) - if err != nil { - return "", err - } - signature := append(padTransportIdentity32(r), padTransportIdentity32(s)...) - return base64.StdEncoding.EncodeToString(signature), nil -} - -func padTransportIdentity32(value *big.Int) []byte { - bytes := value.Bytes() - if len(bytes) >= 32 { - return bytes[len(bytes)-32:] - } - padded := make([]byte, 32) - copy(padded[32-len(bytes):], bytes) - return padded -} - func transportIdentityFromRequest(r *http.Request) (*TransportIdentity, error) { nodeID := strings.TrimSpace(r.Header.Get(sourceNodeHeader)) validatorAddress := strings.TrimSpace(r.Header.Get(sourceValidatorHeader)) @@ -268,12 +267,13 @@ func transportIdentityFromRequest(r *http.Request) (*TransportIdentity, error) { publicKey := strings.TrimSpace(r.Header.Get(sourcePublicKeyHeader)) signature := strings.TrimSpace(r.Header.Get(sourceSignatureHeader)) signedAtRaw := strings.TrimSpace(r.Header.Get(sourceSignedAtHeader)) + chainID := strings.TrimSpace(r.Header.Get(sourceChainIDHeader)) - if nodeID == "" && validatorAddress == "" && payload == "" && publicKey == "" && signature == "" && signedAtRaw == "" { + if nodeID == "" && validatorAddress == "" && payload == "" && publicKey == "" && signature == "" && signedAtRaw == "" && chainID == "" { return nil, nil } - if validatorAddress == "" && payload == "" && publicKey == "" && signature == "" && signedAtRaw == "" { - return nil, nil + if chainID == "" || validatorAddress == "" || payload == "" || publicKey == "" || signature == "" || signedAtRaw == "" { + return nil, errMissingTransportIdentityFields } signedAt, err := time.Parse(time.RFC3339Nano, signedAtRaw) @@ -281,6 +281,8 @@ func transportIdentityFromRequest(r *http.Request) (*TransportIdentity, error) { return nil, errTransportIdentityTimestamp } identity := &TransportIdentity{ + ChainID: chainID, + Domain: protocol.TransportIdentityDomain, NodeID: nodeID, ValidatorAddress: validatorAddress, Payload: payload, @@ -306,6 +308,9 @@ func verifyPeerTransportIdentity(status StatusResponse, now time.Time) (bool, st } return false, "" } + if status.Identity.ChainID != status.ChainID { + return false, errTransportIdentityChainMismatch.Error() + } if status.Identity.NodeID != status.NodeID { return false, errTransportIdentityNodeMismatch.Error() } From 508e2d75261b8410ec81e00cafda5d3a9d0cf692 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:57:42 +0200 Subject: [PATCH 09/88] sign peer requests with request-bound proofs --- internal/api/peer_transport.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/api/peer_transport.go b/internal/api/peer_transport.go index bf10284e..fcdbcaaa 100644 --- a/internal/api/peer_transport.go +++ b/internal/api/peer_transport.go @@ -72,7 +72,7 @@ func (t *httpPeerTransport) FetchSnapshot(peerURL string) (ledger.Snapshot, erro if err != nil { return ledger.Snapshot{}, err } - if err := t.applyPeerHeaders(request); err != nil { + if err := t.applyPeerHeaders(request, nil); err != nil { return ledger.Snapshot{}, err } @@ -123,7 +123,7 @@ func (t *httpPeerTransport) postJSON(target string, payload any) error { return err } request.Header.Set("Content-Type", "application/json") - if err := t.applyPeerHeaders(request); err != nil { + if err := t.applyPeerHeaders(request, body); err != nil { return err } @@ -138,20 +138,22 @@ func (t *httpPeerTransport) postJSON(target string, payload any) error { return nil } -func (t *httpPeerTransport) applyPeerHeaders(request *http.Request) error { - request.Header.Set(sourceNodeHeader, t.sourceNode) +func (t *httpPeerTransport) applyPeerHeaders(request *http.Request, body []byte) error { if t.identitySigner == nil { - return nil + return errMissingRequestProof } - - identity, err := t.identitySigner.Build(time.Now().UTC()) + proof, err := t.identitySigner.buildRequestProof(request.Method, canonicalRequestPath(request), body, time.Now().UTC()) if err != nil { return err } - request.Header.Set(sourceValidatorHeader, identity.ValidatorAddress) - request.Header.Set(sourceIdentityPayloadHeader, identity.Payload) - request.Header.Set(sourcePublicKeyHeader, identity.PublicKey) - request.Header.Set(sourceSignatureHeader, identity.Signature) - request.Header.Set(sourceSignedAtHeader, identity.SignedAt.UTC().Format(time.RFC3339Nano)) + request.Header.Set(sourceNodeHeader, proof.NodeID) + request.Header.Set(sourceValidatorHeader, proof.ValidatorAddress) + request.Header.Set(sourceIdentityPayloadHeader, proof.Payload) + request.Header.Set(sourcePublicKeyHeader, proof.PublicKey) + request.Header.Set(sourceSignatureHeader, proof.Signature) + request.Header.Set(sourceSignedAtHeader, proof.SignedAt.UTC().Format(time.RFC3339Nano)) + request.Header.Set(sourceChainIDHeader, proof.ChainID) + request.Header.Set(sourceRequestDomainHeader, proof.Domain) + request.Header.Set(sourceRequestNonceHeader, proof.Nonce) return nil } From 695e86a1e9b58662ea420ead310dfe7e4e1d0df0 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 15:58:11 +0200 Subject: [PATCH 10/88] enforce chain-aware replay-safe peer admission --- internal/api/peer_admission.go | 38 +++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/internal/api/peer_admission.go b/internal/api/peer_admission.go index 18193ab2..86bfa02f 100644 --- a/internal/api/peer_admission.go +++ b/internal/api/peer_admission.go @@ -9,7 +9,7 @@ import ( ) var ( - errPeerIdentityRequired = errors.New("peer request must include a signed transport identity") + errPeerIdentityRequired = errors.New("peer request must include a signed request proof") errPeerValidatorNotAllowed = errors.New("peer validator is not admitted by local policy") ) @@ -71,6 +71,9 @@ func (s *Server) buildPeerView(peerURL string, status StatusResponse, now time.T admissionError := "" switch { + case status.ChainID != s.config.ChainID: + admitted = false + admissionError = fmt.Sprintf("peer chain %s does not match local chain %s", status.ChainID, s.config.ChainID) case s.peerIdentityRequired() && status.Identity == nil: admitted = false admissionError = "peer does not expose a signed transport identity" @@ -163,26 +166,37 @@ func (s *Server) admittedPeerURLs() []string { } func (s *Server) validatePeerRequest(r *http.Request) error { - identity, err := transportIdentityFromRequest(r) - if err != nil { - return err - } if requestSourceNode(r) == "" { return nil } - if !s.peerIdentityRequired() { - return nil + + proof, err := validateAndRememberRequestProof(s, r) + if err != nil { + return err } - if identity == nil { + if proof == nil { return errPeerIdentityRequired } allowed := s.allowedPeerValidators() - if len(allowed) == 0 { - return nil + if len(allowed) > 0 { + if _, ok := allowed[proof.ValidatorAddress]; !ok { + return errPeerValidatorNotAllowed + } } - if _, ok := allowed[identity.ValidatorAddress]; !ok { - return errPeerValidatorNotAllowed + + validatorSet := s.ledger.ValidatorSet() + if len(validatorSet.Validators) > 0 { + active := false + for _, validator := range validatorSet.Validators { + if validator.Address == proof.ValidatorAddress { + active = true + break + } + } + if !active { + return errPeerValidatorNotAllowed + } } return nil } From a0b1530f50ee96fb6ca9c43b6e3b5535ac659545 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:00:16 +0200 Subject: [PATCH 11/88] add versioned state commitment domain --- internal/protocol/security.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/protocol/security.go b/internal/protocol/security.go index b74c3f81..9be4b743 100644 --- a/internal/protocol/security.go +++ b/internal/protocol/security.go @@ -14,6 +14,7 @@ const ( ConsensusVoteDomain = "zephyr/consensus/vote/v1" TransportIdentityDomain = "zephyr/transport/identity/v1" TransportRequestDomain = "zephyr/transport/request/v1" + StateDomain = "zephyr/state/v1" SnapshotDomain = "zephyr/snapshot/v1" ) From 62341f070d5ebb8eea97a2a3fb1e4ad06c449495 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:01:13 +0200 Subject: [PATCH 12/88] add deterministic chain-bound state commitments --- internal/ledger/state_commitment.go | 103 ++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 internal/ledger/state_commitment.go diff --git a/internal/ledger/state_commitment.go b/internal/ledger/state_commitment.go new file mode 100644 index 00000000..776d0ef8 --- /dev/null +++ b/internal/ledger/state_commitment.go @@ -0,0 +1,103 @@ +package ledger + +import ( + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "sort" + "strings" + + "github.com/zephyr-chain/zephyr-chain/internal/dpos" + "github.com/zephyr-chain/zephyr-chain/internal/protocol" +) + +var ErrInvalidStateRoot = errors.New("invalid committed state root") + +type committedAccount struct { + Address string `json:"address"` + Balance uint64 `json:"balance"` + Nonce uint64 `json:"nonce"` +} + +type committedValidator struct { + Address string `json:"address"` + CommissionRate float64 `json:"commissionRate"` + DelegatedStake uint64 `json:"delegatedStake"` + Rank int `json:"rank"` + SelfStake uint64 `json:"selfStake"` + VotingPower uint64 `json:"votingPower"` +} + +type committedStatePayload struct { + Accounts []committedAccount `json:"accounts"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` + ElectionConfig dpos.ElectionConfig `json:"electionConfig"` + ValidatorSetVersion uint64 `json:"validatorSetVersion"` + Validators []committedValidator `json:"validators"` +} + +func StateRoot(chainID string, accounts map[string]AccountState, snapshot ValidatorSnapshot) (string, error) { + chainID = strings.TrimSpace(chainID) + if err := protocol.ValidateChainID(chainID); err != nil { + return "", ErrInvalidStateRoot + } + + addresses := make([]string, 0, len(accounts)) + for address := range accounts { + addresses = append(addresses, address) + } + sort.Strings(addresses) + committedAccounts := make([]committedAccount, 0, len(addresses)) + for _, address := range addresses { + account := accounts[address] + if account.Address != "" && account.Address != address { + return "", ErrInvalidStateRoot + } + committedAccounts = append(committedAccounts, committedAccount{ + Address: address, + Balance: account.Balance, + Nonce: account.Nonce, + }) + } + + snapshot = normalizeValidatorSnapshot(snapshot) + validators := append([]dpos.Validator(nil), snapshot.Validators...) + sort.Slice(validators, func(i, j int) bool { + if validators[i].Rank != validators[j].Rank { + return validators[i].Rank < validators[j].Rank + } + return validators[i].Address < validators[j].Address + }) + committedValidators := make([]committedValidator, 0, len(validators)) + for _, validator := range validators { + committedValidators = append(committedValidators, committedValidator{ + Address: validator.Address, + CommissionRate: validator.CommissionRate, + DelegatedStake: validator.DelegatedStake, + Rank: validator.Rank, + SelfStake: validator.SelfStake, + VotingPower: validator.VotingPower, + }) + } + + payload, err := json.Marshal(committedStatePayload{ + Accounts: committedAccounts, + ChainID: chainID, + Domain: protocol.StateDomain, + ElectionConfig: snapshot.ElectionConfig, + ValidatorSetVersion: snapshot.Version, + Validators: committedValidators, + }) + if err != nil { + return "", err + } + digest := sha256.Sum256(payload) + return hex.EncodeToString(digest[:]), nil +} + +func stateRootFromState(chainID string, state persistedState) (string, error) { + state = normalizeState(state) + return StateRoot(chainID, state.Accounts, state.ValidatorSnapshot) +} From 464a054b9d404fb1f7d55d1fad3729be6da8a9d9 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:02:03 +0200 Subject: [PATCH 13/88] validate snapshots against quorum-signed committed state --- internal/ledger/snapshot_security.go | 224 +++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 internal/ledger/snapshot_security.go diff --git a/internal/ledger/snapshot_security.go b/internal/ledger/snapshot_security.go new file mode 100644 index 00000000..1d9be43a --- /dev/null +++ b/internal/ledger/snapshot_security.go @@ -0,0 +1,224 @@ +package ledger + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/zephyr-chain/zephyr-chain/internal/protocol" + "github.com/zephyr-chain/zephyr-chain/internal/tx" +) + +var ( + ErrInvalidSnapshot = errors.New("invalid peer snapshot") + ErrSnapshotChainMismatch = errors.New("snapshot chain ID does not match local chain") + ErrSnapshotProofInvalid = errors.New("invalid snapshot proof") + ErrSnapshotQuorumRequired = errors.New("snapshot does not have trusted validator quorum") +) + +type SnapshotProof struct { + BlockHash string `json:"blockHash"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` + Height uint64 `json:"height"` + Payload string `json:"payload"` + PublicKey string `json:"publicKey"` + Signature string `json:"signature"` + Signer string `json:"signer"` + StateCommitment string `json:"stateCommitment"` + ValidatorSetVersion uint64 `json:"validatorSetVersion"` +} + +type canonicalSnapshotProof struct { + BlockHash string `json:"blockHash"` + ChainID string `json:"chainId"` + Domain string `json:"domain"` + Height uint64 `json:"height"` + Signer string `json:"signer"` + StateCommitment string `json:"stateCommitment"` + ValidatorSetVersion uint64 `json:"validatorSetVersion"` +} + +func (p SnapshotProof) CanonicalPayload() string { + payload, _ := json.Marshal(canonicalSnapshotProof{ + BlockHash: p.BlockHash, + ChainID: strings.TrimSpace(p.ChainID), + Domain: strings.TrimSpace(p.Domain), + Height: p.Height, + Signer: p.Signer, + StateCommitment: p.StateCommitment, + ValidatorSetVersion: p.ValidatorSetVersion, + }) + return string(payload) +} + +func BuildSnapshotProofTemplate(snapshot Snapshot, chainID string, signer string) (SnapshotProof, error) { + if err := ValidateSnapshotCommittedState(snapshot, chainID); err != nil { + return SnapshotProof{}, err + } + latest := snapshot.Blocks[len(snapshot.Blocks)-1] + return SnapshotProof{ + BlockHash: latest.Hash, + ChainID: strings.TrimSpace(chainID), + Domain: protocol.SnapshotDomain, + Height: latest.Height, + Signer: signer, + StateCommitment: latest.StateRoot, + ValidatorSetVersion: snapshot.ValidatorSnapshot.Version, + }, nil +} + +func ValidateSnapshotCommittedState(snapshot Snapshot, chainID string) error { + chainID = strings.TrimSpace(chainID) + if err := protocol.ValidateChainID(chainID); err != nil { + return ErrSnapshotChainMismatch + } + if len(snapshot.Blocks) == 0 { + return fmt.Errorf("%w: no committed blocks", ErrInvalidSnapshot) + } + + seenTransactions := make(map[string]struct{}) + previousHash := "" + for index, block := range snapshot.Blocks { + expectedHeight := uint64(index + 1) + if block.ChainID != chainID { + return ErrSnapshotChainMismatch + } + if block.Height != expectedHeight || block.PreviousHash != previousHash || block.StateRoot == "" { + return ErrInvalidSnapshot + } + if block.TransactionCount != len(block.Transactions) || len(block.TransactionIDs) != len(block.Transactions) { + return ErrInvalidSnapshot + } + for txIndex, envelope := range block.Transactions { + if err := envelope.ValidateForChain(chainID); err != nil { + return ErrInvalidSnapshot + } + id := tx.ID(envelope) + if block.TransactionIDs[txIndex] != id { + return ErrInvalidSnapshot + } + if _, exists := seenTransactions[id]; exists { + return ErrInvalidSnapshot + } + seenTransactions[id] = struct{}{} + } + if block.Hash != blockHash(block) { + return ErrInvalidSnapshot + } + previousHash = block.Hash + } + + latest := snapshot.Blocks[len(snapshot.Blocks)-1] + root, err := StateRoot(chainID, snapshot.Accounts, snapshot.ValidatorSnapshot) + if err != nil || root != latest.StateRoot { + return ErrInvalidStateRoot + } + if _, ok := sumValidatorVotingPower(snapshot.ValidatorSnapshot.Validators); !ok { + return ErrVotingPowerOverflow + } + return nil +} + +func ValidateSnapshotProof(snapshot Snapshot, chainID string, proof SnapshotProof, trusted ValidatorSnapshot) (uint64, error) { + if err := ValidateSnapshotCommittedState(snapshot, chainID); err != nil { + return 0, err + } + latest := snapshot.Blocks[len(snapshot.Blocks)-1] + if strings.TrimSpace(proof.ChainID) != strings.TrimSpace(chainID) { + return 0, ErrSnapshotChainMismatch + } + if proof.Domain != protocol.SnapshotDomain || proof.Height != latest.Height || proof.BlockHash != latest.Hash || proof.StateCommitment != latest.StateRoot || proof.ValidatorSetVersion != snapshot.ValidatorSnapshot.Version { + return 0, ErrSnapshotProofInvalid + } + if proof.Signer == "" || proof.Payload == "" || proof.PublicKey == "" || proof.Signature == "" || proof.Payload != proof.CanonicalPayload() { + return 0, ErrSnapshotProofInvalid + } + address, err := tx.DeriveAddressFromPublicKey(proof.PublicKey) + if err != nil || address != proof.Signer { + return 0, ErrSnapshotProofInvalid + } + if err := tx.VerifySignature(proof.PublicKey, proof.Payload, proof.Signature); err != nil { + return 0, ErrSnapshotProofInvalid + } + + trusted = normalizeValidatorSnapshot(trusted) + for _, validator := range trusted.Validators { + if validator.Address == proof.Signer { + return validator.VotingPower, nil + } + } + return 0, ErrSnapshotProofInvalid +} + +func ValidateSnapshotQuorum(snapshot Snapshot, chainID string, proofs []SnapshotProof, trusted ValidatorSnapshot) error { + trusted = normalizeValidatorSnapshot(trusted) + total := totalVotingPower(trusted) + quorum := quorumVotingPower(total) + if total == 0 || quorum == 0 { + return ErrSnapshotQuorumRequired + } + + seen := make(map[string]struct{}) + var signedPower uint64 + for _, proof := range proofs { + if _, exists := seen[proof.Signer]; exists { + continue + } + power, err := ValidateSnapshotProof(snapshot, chainID, proof, trusted) + if err != nil { + continue + } + next, ok := addUint64(signedPower, power) + if !ok { + return ErrVotingPowerOverflow + } + signedPower = next + seen[proof.Signer] = struct{}{} + } + if signedPower < quorum { + return ErrSnapshotQuorumRequired + } + return nil +} + +func (s *Store) RestoreQuorumSnapshot(snapshot Snapshot, chainID string, proofs []SnapshotProof, trusted ValidatorSnapshot, now time.Time) error { + if err := ValidateSnapshotQuorum(snapshot, chainID, proofs, trusted); err != nil { + return err + } + if now.IsZero() { + now = time.Now().UTC() + } + + s.mu.Lock() + defer s.mu.Unlock() + localState := s.snapshotLocked() + + incoming := persistedFromSnapshot(snapshot) + incoming.Mempool = make([]MempoolEntry, 0) + incoming.AppliedFundingIDs = make([]string, 0) + incoming.RoundState = ConsensusRoundState{Height: uint64(len(incoming.Blocks) + 1)} + incoming.Proposals = make([]consensus.Proposal, 0) + incoming.Votes = make([]VoteRecord, 0) + incoming.CommitCertificates = make([]CommitCertificate, 0) + incoming.ConsensusActions = normalizeConsensusActions(localState.ConsensusActions) + incoming.ConsensusDiagnostics = normalizeConsensusDiagnostics(localState.ConsensusDiagnostics) + incoming.PeerSyncIncidents = normalizePeerSyncIncidents(localState.PeerSyncIncidents) + incoming.CommittedTransactionIDs = committedIDsFromBlocks(incoming.Blocks) + incoming = completeConsensusActionsForHeightInState(incoming, uint64(len(incoming.Blocks)), now.UTC(), "quorum-validated state restored from peer snapshot") + if err := s.writeState(incoming); err != nil { + return err + } + s.applyStateLocked(incoming) + return nil +} + +func committedIDsFromBlocks(blocks []Block) []string { + ids := make([]string, 0) + for _, block := range blocks { + ids = append(ids, block.TransactionIDs...) + } + return uniqueSortedStrings(ids) +} From a20484fbe54addd4fbadaa9a8239bc1a00703a40 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:02:22 +0200 Subject: [PATCH 14/88] add versioned block signing domain --- internal/protocol/security.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/protocol/security.go b/internal/protocol/security.go index 9be4b743..68acb079 100644 --- a/internal/protocol/security.go +++ b/internal/protocol/security.go @@ -10,6 +10,7 @@ const ( DefaultChainID = "zephyr-devnet-1" TransactionDomain = "zephyr/transaction/v1" + BlockDomain = "zephyr/block/v1" ConsensusProposalDomain = "zephyr/consensus/proposal/v1" ConsensusVoteDomain = "zephyr/consensus/vote/v1" TransportIdentityDomain = "zephyr/transport/identity/v1" From 06e377606f30c6eb5745050e3cc53bf2b8b0cb01 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:03:06 +0200 Subject: [PATCH 15/88] commit chain and state root into block identity --- internal/consensus/messages.go | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/internal/consensus/messages.go b/internal/consensus/messages.go index 11358989..c38fe471 100644 --- a/internal/consensus/messages.go +++ b/internal/consensus/messages.go @@ -21,6 +21,7 @@ var ( ErrInvalidChainID = errors.New("consensus message chain ID does not match local chain") ErrInvalidDomain = errors.New("invalid consensus signing domain") ErrInvalidHash = errors.New("invalid block hash") + ErrInvalidStateRoot = errors.New("invalid state root") ErrInvalidHeight = errors.New("height must be greater than zero") ErrInvalidProducedAt = errors.New("producedAt must be set") ErrInvalidTransactionID = errors.New("invalid transaction ID") @@ -37,6 +38,7 @@ type Proposal struct { Round uint64 `json:"round"` BlockHash string `json:"blockHash"` PreviousHash string `json:"previousHash"` + StateRoot string `json:"stateRoot"` ProducedAt time.Time `json:"producedAt"` TransactionIDs []string `json:"transactionIds"` Transactions []tx.Envelope `json:"transactions"` @@ -69,6 +71,7 @@ type canonicalProposal struct { ProducedAt string `json:"producedAt"` Proposer string `json:"proposer"` Round uint64 `json:"round"` + StateRoot string `json:"stateRoot"` TransactionIDs []string `json:"transactionIds"` } @@ -81,16 +84,24 @@ type canonicalVote struct { Voter string `json:"voter"` } -func BlockHash(height uint64, previousHash string, producedAt time.Time, transactionIDs []string) string { - payload, _ := json.Marshal(struct { - Height uint64 `json:"height"` - PreviousHash string `json:"previousHash"` - ProducedAt string `json:"producedAt"` - TransactionIDs []string `json:"transactionIds"` - }{ +type canonicalBlock struct { + ChainID string `json:"chainId"` + Domain string `json:"domain"` + Height uint64 `json:"height"` + PreviousHash string `json:"previousHash"` + ProducedAt string `json:"producedAt"` + StateRoot string `json:"stateRoot"` + TransactionIDs []string `json:"transactionIds"` +} + +func BlockHash(chainID string, height uint64, previousHash string, producedAt time.Time, stateRoot string, transactionIDs []string) string { + payload, _ := json.Marshal(canonicalBlock{ + ChainID: strings.TrimSpace(chainID), + Domain: protocol.BlockDomain, Height: height, PreviousHash: previousHash, ProducedAt: producedAt.UTC().Format(time.RFC3339Nano), + StateRoot: stateRoot, TransactionIDs: append([]string(nil), transactionIDs...), }) @@ -108,6 +119,7 @@ func (p Proposal) CanonicalPayload() string { ProducedAt: p.ProducedAt.UTC().Format(time.RFC3339Nano), Proposer: p.Proposer, Round: p.Round, + StateRoot: p.StateRoot, TransactionIDs: append([]string(nil), p.TransactionIDs...), }) @@ -115,13 +127,13 @@ func (p Proposal) CanonicalPayload() string { } func (p Proposal) CandidateHash() string { - return BlockHash(p.Height, p.PreviousHash, p.ProducedAt, p.TransactionIDs) + return BlockHash(p.ChainID, p.Height, p.PreviousHash, p.ProducedAt, p.StateRoot, p.TransactionIDs) } func (p Proposal) ValidateStatic() error { chainID := strings.TrimSpace(p.ChainID) domain := strings.TrimSpace(p.Domain) - if chainID == "" || domain == "" || p.BlockHash == "" || p.Proposer == "" || p.Payload == "" || p.PublicKey == "" || p.Signature == "" || len(p.TransactionIDs) == 0 { + if chainID == "" || domain == "" || p.BlockHash == "" || p.StateRoot == "" || p.Proposer == "" || p.Payload == "" || p.PublicKey == "" || p.Signature == "" || len(p.TransactionIDs) == 0 { return ErrMissingFields } if err := protocol.ValidateChainID(chainID); err != nil { @@ -139,6 +151,9 @@ func (p Proposal) ValidateStatic() error { if err := validateHash(p.BlockHash, false); err != nil { return err } + if err := validateHash(p.StateRoot, false); err != nil { + return ErrInvalidStateRoot + } if err := validateHash(p.PreviousHash, true); err != nil { return err } From 68f639a1ace71a8d6753424b097c9956e1425b09 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:03:21 +0200 Subject: [PATCH 16/88] produce certified blocks with chain-bound state roots --- internal/ledger/certified_production.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/ledger/certified_production.go b/internal/ledger/certified_production.go index 5831389b..bb1272e9 100644 --- a/internal/ledger/certified_production.go +++ b/internal/ledger/certified_production.go @@ -7,20 +7,23 @@ import ( "github.com/zephyr-chain/zephyr-chain/internal/tx" ) -func produceCertifiedBlockFromState(state persistedState, producedAt time.Time) (persistedState, Block, error) { +func produceCertifiedBlockFromState(state persistedState, producedAt time.Time, chainID string) (persistedState, Block, error) { state = normalizeState(state) proposal, err := proposalForProduction(state, producedAt, true) if err != nil { return state, Block{}, err } + if proposal.ChainID != chainID { + return state, Block{}, ErrInvalidBlock + } block := blockFromProposal(*proposal) if err := validateBlockConsensus(state, block, true); err != nil { return state, Block{}, err } - nextState, err := importBlockIntoState(state, block) + nextState, err := importBlockIntoState(state, block, chainID) if err != nil { return state, Block{}, err } @@ -67,8 +70,10 @@ func proposalForProduction(state persistedState, producedAt time.Time, requireCe func blockFromProposal(proposal consensus.Proposal) Block { block := Block{ + ChainID: proposal.ChainID, Height: proposal.Height, PreviousHash: proposal.PreviousHash, + StateRoot: proposal.StateRoot, ProducedAt: proposal.ProducedAt, TransactionCount: len(proposal.Transactions), TransactionIDs: append([]string(nil), proposal.TransactionIDs...), From cf309b49a512156f78bd60fffeb7366dfb4fa583 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:06:25 +0200 Subject: [PATCH 17/88] sign committed snapshots with validator proofs --- internal/api/snapshot_proof.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 internal/api/snapshot_proof.go diff --git a/internal/api/snapshot_proof.go b/internal/api/snapshot_proof.go new file mode 100644 index 00000000..d8a7eee7 --- /dev/null +++ b/internal/api/snapshot_proof.go @@ -0,0 +1,30 @@ +package api + +import ( + "errors" + + "github.com/zephyr-chain/zephyr-chain/internal/ledger" + "github.com/zephyr-chain/zephyr-chain/internal/tx" +) + +var errSnapshotSignerRequired = errors.New("snapshot serving requires an active validator signer") + +func (s *Server) signedSnapshot() (ledger.Snapshot, error) { + if s.identitySigner == nil { + return ledger.Snapshot{}, errSnapshotSignerRequired + } + + snapshot := s.ledger.Snapshot() + proof, err := ledger.BuildSnapshotProofTemplate(snapshot, s.config.ChainID, s.identitySigner.validatorAddress) + if err != nil { + return ledger.Snapshot{}, err + } + proof.PublicKey = s.identitySigner.publicKey + proof.Payload = proof.CanonicalPayload() + proof.Signature, err = tx.SignPayload(s.identitySigner.privateKey, proof.Payload) + if err != nil { + return ledger.Snapshot{}, err + } + snapshot.Proof = proof + return snapshot, nil +} From 102d4ec5920d3652a401f55a878258e8697d324c Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:06:40 +0200 Subject: [PATCH 18/88] leave internal request authentication to endpoint handlers --- internal/api/public_handler.go | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/internal/api/public_handler.go b/internal/api/public_handler.go index d0b142cb..081d2d7d 100644 --- a/internal/api/public_handler.go +++ b/internal/api/public_handler.go @@ -31,15 +31,9 @@ func (s *Server) PublicHandler(options PublicHandlerOptions) http.Handler { r.Body = http.MaxBytesReader(w, r.Body, maxPublicRequestBodyBytes) } - if r.URL.Path == "/v1/internal/snapshot" { - if requestSourceNode(r) == "" { - writeJSON(w, http.StatusForbidden, map[string]string{"error": "internal snapshot requires a peer source"}) - return - } - if err := s.validatePeerRequest(r); err != nil { - writeJSON(w, statusForError(err), map[string]string{"error": err.Error()}) - return - } + if r.URL.Path == "/v1/internal/snapshot" && requestSourceNode(r) == "" { + writeJSON(w, http.StatusForbidden, map[string]string{"error": "internal snapshot requires a peer source"}) + return } s.mux.ServeHTTP(w, r) From 82e5e5bc561737ad3daab9dccee6298bdf228924 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:07:57 +0200 Subject: [PATCH 19/88] add temporary protocol backend integration verifier --- .../workflows/protocol-backend-refresh.yml | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 .github/workflows/protocol-backend-refresh.yml diff --git a/.github/workflows/protocol-backend-refresh.yml b/.github/workflows/protocol-backend-refresh.yml new file mode 100644 index 00000000..8629fca9 --- /dev/null +++ b/.github/workflows/protocol-backend-refresh.yml @@ -0,0 +1,206 @@ +name: Protocol backend refresh + +on: + pull_request: + +permissions: + contents: write + +jobs: + refresh: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - name: Checkout protocol branch + uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-hardening + fetch-depth: 0 + + - name: Integrate chain identity, state roots, and snapshot quorum + shell: bash + run: | + python - <<'PY' + from pathlib import Path + import re + + def replace_exact(path, old, new, expected=1): + p = Path(path) + s = p.read_text() + count = s.count(old) + if count != expected: + raise SystemExit(f'{path}: expected {expected} occurrences of {old!r}, found {count}') + p.write_text(s.replace(old, new)) + + def replace_regex(path, pattern, replacement, expected=1): + p = Path(path) + s = p.read_text() + s2, count = re.subn(pattern, replacement, s, flags=re.S) + if count != expected: + raise SystemExit(f'{path}: expected {expected} regex replacements for {pattern!r}, found {count}') + p.write_text(s2) + + store = 'internal/ledger/store.go' + replace_exact(store, + '"github.com/zephyr-chain/zephyr-chain/internal/dpos"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', + '"github.com/zephyr-chain/zephyr-chain/internal/dpos"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"') + replace_exact(store, + 'type Block struct {\n\tHeight uint64 `json:"height"`', + 'type Block struct {\n\tChainID string `json:"chainId"`\n\tHeight uint64 `json:"height"`') + replace_exact(store, + '\tPreviousHash string `json:"previousHash"`\n\tProducedAt', + '\tPreviousHash string `json:"previousHash"`\n\tStateRoot string `json:"stateRoot"`\n\tProducedAt') + replace_exact(store, + '\tPeerSyncIncidents []PeerSyncIncident `json:"peerSyncIncidents"`\n}', + '\tPeerSyncIncidents []PeerSyncIncident `json:"peerSyncIncidents"`\n\tProof SnapshotProof `json:"proof"`\n}', 1) + replace_exact(store, + 'type Store struct {\n\tmu sync.RWMutex', + 'type Store struct {\n\tmu sync.RWMutex\n\tchainID string') + replace_exact(store, + 'func NewStore(dataDir string) (*Store, error) {\n\tif dataDir == "" {', + 'func NewStore(dataDir string) (*Store, error) {\n\treturn NewStoreWithChainID(dataDir, protocol.DefaultChainID)\n}\n\nfunc NewStoreWithChainID(dataDir string, chainID string) (*Store, error) {\n\tchainID = protocol.ConfiguredChainID(chainID)\n\tif err := protocol.ValidateChainID(chainID); err != nil {\n\t\treturn nil, err\n\t}\n\tif dataDir == "" {') + replace_exact(store, + '\tstore := &Store{\n\t\tdataDir:', + '\tstore := &Store{\n\t\tchainID: chainID,\n\t\tdataDir:') + replace_exact(store, + '\t_, block, err := produceBlockFromState(state, maxTransactions, producedAt)', + '\t_, block, err := produceBlockFromState(state, maxTransactions, producedAt, s.chainID)') + replace_exact(store, + '\t\tnextState, block, err = produceCertifiedBlockFromState(state, producedAt)', + '\t\tnextState, block, err = produceCertifiedBlockFromState(state, producedAt, s.chainID)') + replace_exact(store, + '\t\tnextState, block, err = produceBlockFromState(state, maxTransactions, producedAt)', + '\t\tnextState, block, err = produceBlockFromState(state, maxTransactions, producedAt, s.chainID)') + replace_exact(store, + '\tnextState, err := importBlockIntoState(state, block)', + '\tnextState, err := importBlockIntoState(state, block, s.chainID)') + replace_exact(store, + 'func produceBlockFromState(state persistedState, maxTransactions int, producedAt time.Time) (persistedState, Block, error) {', + 'func produceBlockFromState(state persistedState, maxTransactions int, producedAt time.Time, chainID string) (persistedState, Block, error) {') + replace_exact(store, + '\tif producedAt.IsZero() {\n\t\tproducedAt = time.Now().UTC()\n\t}\n\tblock := Block{', + '\trootState := state\n\trootState.Accounts = accounts\n\tstateRoot, err := stateRootFromState(chainID, rootState)\n\tif err != nil {\n\t\treturn state, Block{}, err\n\t}\n\n\tif producedAt.IsZero() {\n\t\tproducedAt = time.Now().UTC()\n\t}\n\tblock := Block{\n\t\tChainID: chainID,\n\t\tStateRoot: stateRoot,') + replace_exact(store, + 'func importBlockIntoState(state persistedState, block Block) (persistedState, error) {\n\tstate = normalizeState(state)', + 'func importBlockIntoState(state persistedState, block Block, chainID string) (persistedState, error) {\n\tstate = normalizeState(state)\n\tif block.ChainID != chainID || block.StateRoot == "" {\n\t\treturn state, ErrInvalidBlock\n\t}') + replace_exact(store, + '\t\tif err := envelope.ValidateStatic(); err != nil {', + '\t\tif err := envelope.ValidateForChain(chainID); err != nil {', 1) + replace_exact(store, + '\tsanitized := Block{\n\t\tHeight:', + '\trootState := state\n\trootState.Accounts = accounts\n\tstateRoot, err := stateRootFromState(chainID, rootState)\n\tif err != nil || stateRoot != block.StateRoot {\n\t\treturn state, ErrBlockInvariant\n\t}\n\n\tsanitized := Block{\n\t\tChainID: chainID,\n\t\tStateRoot: stateRoot,\n\t\tHeight:') + replace_exact(store, + 'return consensus.BlockHash(block.Height, block.PreviousHash, block.ProducedAt, block.TransactionIDs)', + 'return consensus.BlockHash(block.ChainID, block.Height, block.PreviousHash, block.ProducedAt, block.StateRoot, block.TransactionIDs)') + replace_exact(store, + '\t\tPeerSyncIncidents: clonePeerSyncIncidents(state.PeerSyncIncidents),\n\t}', + '\t\tPeerSyncIncidents: clonePeerSyncIncidents(state.PeerSyncIncidents),\n\t}', 1) + + cs = 'internal/ledger/consensus_state.go' + replace_exact(cs, + '\tstate := s.snapshotLocked()\n\tnextState, err := recordProposalIntoState(state, proposal)', + '\tif err := proposal.ValidateForChain(s.chainID); err != nil {\n\t\treturn err\n\t}\n\tstate := s.snapshotLocked()\n\tnextState, err := recordProposalIntoState(state, proposal)') + replace_exact(cs, + '\tstate := s.snapshotLocked()\n\tnextState, tally, certificate, err := recordVoteIntoState(state, vote)', + '\tif err := vote.ValidateForChain(s.chainID); err != nil {\n\t\treturn VoteTally{}, nil, err\n\t}\n\tstate := s.snapshotLocked()\n\tnextState, tally, certificate, err := recordVoteIntoState(state, vote)') + + snap = 'internal/ledger/snapshot_security.go' + replace_exact(snap, + '"time"\n\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"', + '"time"\n\n\t"github.com/zephyr-chain/zephyr-chain/internal/consensus"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"') + + old_restore = 'internal/ledger/peer_snapshot_restore.go' + Path(old_restore).write_text('''package ledger\n\nimport "time"\n\n// RestoreFromPeerSnapshot is intentionally disabled. Peer snapshots require\n// quorum proofs from the locally trusted validator set.\nfunc (s *Store) RestoreFromPeerSnapshot(snapshot Snapshot, now time.Time) error {\n\treturn ErrSnapshotQuorumRequired\n}\n''') + + server = 'internal/api/server.go' + replace_exact(server, + '"github.com/zephyr-chain/zephyr-chain/internal/ledger"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', + '"github.com/zephyr-chain/zephyr-chain/internal/ledger"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"') + replace_exact(server, + 'type Config struct {\n\tDataDir', + 'type Config struct {\n\tChainID string\n\tDataDir') + replace_exact(server, + '\treturn Config{\n\t\tDataDir:', + '\treturn Config{\n\t\tChainID: protocol.DefaultChainID,\n\t\tDataDir:') + replace_exact(server, + 'type StatusResponse struct {\n\tNodeID', + 'type StatusResponse struct {\n\tChainID string `json:"chainId"`\n\tNodeID') + replace_exact(server, + '\tstore, err := ledger.NewStore(config.DataDir)', + '\tstore, err := ledger.NewStoreWithChainID(config.DataDir, config.ChainID)') + replace_exact(server, + '\tresponse := StatusResponse{\n\t\tNodeID:', + '\tresponse := StatusResponse{\n\t\tChainID: s.config.ChainID,\n\t\tNodeID:') + replace_exact(server, + '\tconfig.ValidatorAddress = strings.TrimSpace(config.ValidatorAddress)', + '\tconfig.ChainID = protocol.ConfiguredChainID(config.ChainID)\n\tconfig.ValidatorAddress = strings.TrimSpace(config.ValidatorAddress)') + if 'request.ValidateStatic()' in Path(server).read_text(): + replace_exact(server, 'request.ValidateStatic()', 'request.ValidateForChain(s.config.ChainID)', 1) + replace_exact(server, + 'func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, SnapshotResponse{Snapshot: s.ledger.Snapshot()})\n}', + 'func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tif err := s.validatePeerRequest(r); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsnapshot, err := s.signedSnapshot()\n\tif err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\twriteJSON(w, http.StatusOK, SnapshotResponse{Snapshot: snapshot})\n}') + replace_exact(server, + 'errors.Is(err, tx.ErrInvalidSignature),', + 'errors.Is(err, tx.ErrInvalidSignature),\n\t\terrors.Is(err, tx.ErrNonCanonicalSignature),\n\t\terrors.Is(err, tx.ErrInvalidChainID),\n\t\terrors.Is(err, tx.ErrInvalidDomain),') + replace_exact(server, + 'errors.Is(err, consensus.ErrInvalidSignature),', + 'errors.Is(err, consensus.ErrInvalidSignature),\n\t\terrors.Is(err, consensus.ErrInvalidChainID),\n\t\terrors.Is(err, consensus.ErrInvalidDomain),\n\t\terrors.Is(err, consensus.ErrInvalidStateRoot),') + replace_exact(server, + 'errors.Is(err, errTransportIdentityValidatorMismatch):', + 'errors.Is(err, errTransportIdentityValidatorMismatch),\n\t\terrors.Is(err, errTransportIdentityChainMismatch),\n\t\terrors.Is(err, errMissingRequestProof),\n\t\terrors.Is(err, errInvalidRequestProof),\n\t\terrors.Is(err, errRequestChainMismatch),\n\t\terrors.Is(err, errRequestDomainMismatch),\n\t\terrors.Is(err, errRequestTimestamp),\n\t\terrors.Is(err, ledger.ErrInvalidSnapshot),\n\t\terrors.Is(err, ledger.ErrSnapshotChainMismatch),\n\t\terrors.Is(err, ledger.ErrSnapshotProofInvalid),\n\t\terrors.Is(err, ledger.ErrInvalidStateRoot):') + replace_exact(server, + 'case errors.Is(err, errPeerIdentityRequired),\n\t\terrors.Is(err, errPeerValidatorNotAllowed):', + 'case errors.Is(err, errPeerIdentityRequired),\n\t\terrors.Is(err, errPeerValidatorNotAllowed),\n\t\terrors.Is(err, errRequestReplay),\n\t\terrors.Is(err, errRequestReplayStoreFull),\n\t\terrors.Is(err, ledger.ErrSnapshotQuorumRequired),\n\t\terrors.Is(err, errSnapshotSignerRequired):') + + api_consensus = 'internal/api/consensus_api.go' + replace_exact(api_consensus, + '\tif request.ProposedAt.IsZero() {\n\t\trequest.ProposedAt = time.Now().UTC()\n\t}\n\n\tsourceNode :=', + '\tif request.ProposedAt.IsZero() {\n\t\trequest.ProposedAt = time.Now().UTC()\n\t}\n\tif err := request.ValidateForChain(s.config.ChainID); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsourceNode :=') + replace_exact(api_consensus, + '\tif request.VotedAt.IsZero() {\n\t\trequest.VotedAt = time.Now().UTC()\n\t}\n\n\tsourceNode :=', + '\tif request.VotedAt.IsZero() {\n\t\trequest.VotedAt = time.Now().UTC()\n\t}\n\tif err := request.ValidateForChain(s.config.ChainID); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsourceNode :=') + + automation = 'internal/api/consensus_automation.go' + replace_exact(automation, + '\t\tproposal.PreviousHash = previousProposal.PreviousHash\n\t\tproposal.ProducedAt', + '\t\tproposal.PreviousHash = previousProposal.PreviousHash\n\t\tproposal.StateRoot = previousProposal.StateRoot\n\t\tproposal.ProducedAt') + replace_exact(automation, + '\t\tproposal.PreviousHash = block.PreviousHash\n\t\tproposal.ProducedAt', + '\t\tproposal.PreviousHash = block.PreviousHash\n\t\tproposal.StateRoot = block.StateRoot\n\t\tproposal.ProducedAt') + + peer_sync = 'internal/api/peer_sync.go' + replace_regex(peer_sync, + r'func \(s \*Server\) restoreSnapshotFromPeer\(peerURL string, reason string\) \(peerSnapshotRestoreResult, error\) \{.*?\n\}\n\nfunc \(s \*Server\) broadcastTransaction', + '''func (s *Server) restoreSnapshotFromPeer(peerURL string, reason string) (peerSnapshotRestoreResult, error) {\n\ttrusted := s.ledger.ValidatorSet()\n\tif len(trusted.Validators) == 0 {\n\t\treturn peerSnapshotRestoreResult{}, ledger.ErrSnapshotQuorumRequired\n\t}\n\n\tsnapshot, err := s.fetchPeerSnapshot(peerURL)\n\tif err != nil {\n\t\treturn peerSnapshotRestoreResult{}, err\n\t}\n\tif uint64(len(snapshot.Blocks)) < s.ledger.Status().Height {\n\t\treturn peerSnapshotRestoreResult{Reason: reason}, nil\n\t}\n\tif err := ledger.ValidateSnapshotCommittedState(snapshot, s.config.ChainID); err != nil {\n\t\treturn peerSnapshotRestoreResult{}, err\n\t}\n\n\tproofs := []ledger.SnapshotProof{snapshot.Proof}\n\tfor _, candidateURL := range s.config.PeerURLs {\n\t\tif candidateURL == peerURL {\n\t\t\tcontinue\n\t\t}\n\t\tother, fetchErr := s.fetchPeerSnapshot(candidateURL)\n\t\tif fetchErr != nil || len(other.Blocks) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\totherLatest := other.Blocks[len(other.Blocks)-1]\n\t\tlatest := snapshot.Blocks[len(snapshot.Blocks)-1]\n\t\tif otherLatest.Height != latest.Height || otherLatest.Hash != latest.Hash || otherLatest.StateRoot != latest.StateRoot || other.ValidatorSnapshot.Version != snapshot.ValidatorSnapshot.Version {\n\t\t\tcontinue\n\t\t}\n\t\tproofs = append(proofs, other.Proof)\n\t}\n\n\tnow := time.Now().UTC()\n\tif err := s.ledger.RestoreQuorumSnapshot(snapshot, s.config.ChainID, proofs, trusted, now); err != nil {\n\t\treturn peerSnapshotRestoreResult{}, err\n\t}\n\ts.recordSnapshotRestore(peerURL, snapshot, now)\n\tresult := peerSnapshotRestoreResult{\n\t\tApplied: true,\n\t\tRestoredAt: now,\n\t\tHeight: uint64(len(snapshot.Blocks)),\n\t\tReason: reason,\n\t}\n\tif len(snapshot.Blocks) > 0 {\n\t\tresult.BlockHash = snapshot.Blocks[len(snapshot.Blocks)-1].Hash\n\t}\n\treturn result, nil\n}\n\nfunc (s *Server) broadcastTransaction''') + + main = 'cmd/node/main.go' + replace_exact(main, + '\tif nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {', + '\tif chainID := os.Getenv("ZEPHYR_CHAIN_ID"); chainID != "" {\n\t\tconfig.ChainID = chainID\n\t}\n\tif nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {') + replace_exact(main, + '"zephyr node %s listening on %s', + '"zephyr node %s on chain %s listening on %s') + replace_exact(main, + '\t\tconfig.NodeID,\n\t\taddr,', + '\t\tconfig.NodeID,\n\t\tconfig.ChainID,\n\t\taddr,') + PY + + gofmt -w internal/protocol internal/tx internal/consensus internal/ledger internal/api cmd/node + + - name: Verify backend + run: | + go vet ./... + go test ./... + + - name: Commit verified backend integration + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add internal cmd + if git diff --cached --quiet; then + echo "No backend integration changes to commit." + exit 0 + fi + git commit -m "integrate chain-bound protocol and snapshot quorum" + git push origin HEAD:chatgpt/protocol-hardening From fa4e469c61e469aecf9d5314dadc1a26f8a9d637 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:08:55 +0200 Subject: [PATCH 20/88] fix protocol refresh snapshot selector --- .github/workflows/fix-protocol-refresh.yml | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/fix-protocol-refresh.yml diff --git a/.github/workflows/fix-protocol-refresh.yml b/.github/workflows/fix-protocol-refresh.yml new file mode 100644 index 00000000..b1c4abc9 --- /dev/null +++ b/.github/workflows/fix-protocol-refresh.yml @@ -0,0 +1,34 @@ +name: Fix protocol refresh selector + +on: + pull_request: + +permissions: + contents: write + +jobs: + fix: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-hardening + - name: Fix snapshot selector + shell: bash + run: | + python - <<'PY' + from pathlib import Path + p = Path('.github/workflows/protocol-backend-refresh.yml') + s = p.read_text() + old = """ replace_exact(store,\n '\\tPeerSyncIncidents []PeerSyncIncident `json:\"peerSyncIncidents\"`\\n}',\n '\\tPeerSyncIncidents []PeerSyncIncident `json:\"peerSyncIncidents\"`\\n\\tProof SnapshotProof `json:\"proof\"`\\n}', 1)""" + new = """ replace_regex(store,\n r'(type Snapshot struct \\{.*?\\tPeerSyncIncidents \\[\\]PeerSyncIncident `json:\"peerSyncIncidents\"`\\n)\\}',\n r'\\1\\tProof SnapshotProof `json:\"proof\"`\\n}', 1)""" + if old not in s: + raise SystemExit('snapshot selector block not found') + p.write_text(s.replace(old, new)) + PY + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add .github/workflows/protocol-backend-refresh.yml + git commit -m "fix protocol refresh snapshot selector" + git push origin HEAD:chatgpt/protocol-hardening From 6c2d91d5b1edd011cb27b94aca39324496e4eaab Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:10:01 +0200 Subject: [PATCH 21/88] run corrected protocol backend integration --- .../workflows/protocol-backend-refresh-v2.yml | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/protocol-backend-refresh-v2.yml diff --git a/.github/workflows/protocol-backend-refresh-v2.yml b/.github/workflows/protocol-backend-refresh-v2.yml new file mode 100644 index 00000000..07076fac --- /dev/null +++ b/.github/workflows/protocol-backend-refresh-v2.yml @@ -0,0 +1,56 @@ +name: Protocol backend refresh v2 + +on: + pull_request: + +permissions: + contents: write + +jobs: + refresh: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: chatgpt/protocol-hardening + fetch-depth: 0 + - name: Run corrected integration patch + shell: bash + run: | + python - <<'PY' + from pathlib import Path + text = Path('.github/workflows/protocol-backend-refresh.yml').read_text() + marker = " python - <<'PY'\n" + start = text.index(marker) + len(marker) + end = text.index("\n PY\n", start) + lines = text[start:end].splitlines() + script = "\n".join(line[10:] if line.startswith(" ") else line for line in lines) + old = """replace_exact(store, + '\\tPeerSyncIncidents []PeerSyncIncident `json:\"peerSyncIncidents\"`\\n}', + '\\tPeerSyncIncidents []PeerSyncIncident `json:\"peerSyncIncidents\"`\\n\\tProof SnapshotProof `json:\"proof\"`\\n}', 1)""" + new = """replace_regex(store, + r'(type Snapshot struct \\{.*?\\tPeerSyncIncidents \\[\\]PeerSyncIncident `json:\"peerSyncIncidents\"`\\n)\\}', + r'\\1\\tProof SnapshotProof `json:\"proof\"`\\n}', 1)""" + if old not in script: + raise SystemExit('unable to patch snapshot selector in integration script') + script = script.replace(old, new) + exec(compile(script, 'protocol-backend-refresh', 'exec')) + PY + gofmt -w internal/protocol internal/tx internal/consensus internal/ledger internal/api cmd/node + - name: Verify backend + run: | + go vet ./... + go test ./... + - name: Commit verified backend + shell: bash + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add internal cmd + if git diff --cached --quiet; then + echo "No backend changes to commit." + exit 0 + fi + git commit -m "integrate chain-bound protocol and snapshot quorum" + git push origin HEAD:chatgpt/protocol-hardening From 7c90166a8281c274d5a5b2e96abbdc8ace68433e Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:11:23 +0200 Subject: [PATCH 22/88] add protocol backend integration script --- scripts/protocol_backend_refresh.py | 279 ++++++++++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 scripts/protocol_backend_refresh.py diff --git a/scripts/protocol_backend_refresh.py b/scripts/protocol_backend_refresh.py new file mode 100644 index 00000000..8b35f5a8 --- /dev/null +++ b/scripts/protocol_backend_refresh.py @@ -0,0 +1,279 @@ +from pathlib import Path +import re + + +def replace_exact(path, old, new, expected=1): + p = Path(path) + s = p.read_text() + count = s.count(old) + if count != expected: + raise SystemExit(f"{path}: expected {expected} occurrences of {old!r}, found {count}") + p.write_text(s.replace(old, new)) + + +def replace_regex(path, pattern, replacement, expected=1): + p = Path(path) + s = p.read_text() + s2, count = re.subn(pattern, replacement, s, flags=re.S) + if count != expected: + raise SystemExit(f"{path}: expected {expected} regex replacements for {pattern!r}, found {count}") + p.write_text(s2) + + +store = "internal/ledger/store.go" +replace_exact( + store, + '"github.com/zephyr-chain/zephyr-chain/internal/dpos"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', + '"github.com/zephyr-chain/zephyr-chain/internal/dpos"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', +) +replace_exact( + store, + 'type Block struct {\n\tHeight uint64 `json:"height"`', + 'type Block struct {\n\tChainID string `json:"chainId"`\n\tHeight uint64 `json:"height"`', +) +replace_exact( + store, + '\tPreviousHash string `json:"previousHash"`\n\tProducedAt', + '\tPreviousHash string `json:"previousHash"`\n\tStateRoot string `json:"stateRoot"`\n\tProducedAt', +) +replace_regex( + store, + r'(type Snapshot struct \{.*?\tPeerSyncIncidents \[\]PeerSyncIncident `json:"peerSyncIncidents"`\n)\}', + r'\1\tProof SnapshotProof `json:"proof"`\n}', +) +replace_exact( + store, + 'type Store struct {\n\tmu sync.RWMutex', + 'type Store struct {\n\tmu sync.RWMutex\n\tchainID string', +) +replace_exact( + store, + 'func NewStore(dataDir string) (*Store, error) {\n\tif dataDir == "" {', + 'func NewStore(dataDir string) (*Store, error) {\n\treturn NewStoreWithChainID(dataDir, protocol.DefaultChainID)\n}\n\nfunc NewStoreWithChainID(dataDir string, chainID string) (*Store, error) {\n\tchainID = protocol.ConfiguredChainID(chainID)\n\tif err := protocol.ValidateChainID(chainID); err != nil {\n\t\treturn nil, err\n\t}\n\tif dataDir == "" {', +) +replace_exact( + store, + '\tstore := &Store{\n\t\tdataDir:', + '\tstore := &Store{\n\t\tchainID: chainID,\n\t\tdataDir:', +) +replace_exact( + store, + '\t_, block, err := produceBlockFromState(state, maxTransactions, producedAt)', + '\t_, block, err := produceBlockFromState(state, maxTransactions, producedAt, s.chainID)', +) +replace_exact( + store, + '\t\tnextState, block, err = produceCertifiedBlockFromState(state, producedAt)', + '\t\tnextState, block, err = produceCertifiedBlockFromState(state, producedAt, s.chainID)', +) +replace_exact( + store, + '\t\tnextState, block, err = produceBlockFromState(state, maxTransactions, producedAt)', + '\t\tnextState, block, err = produceBlockFromState(state, maxTransactions, producedAt, s.chainID)', +) +replace_exact( + store, + '\tnextState, err := importBlockIntoState(state, block)', + '\tnextState, err := importBlockIntoState(state, block, s.chainID)', +) +replace_exact( + store, + 'func produceBlockFromState(state persistedState, maxTransactions int, producedAt time.Time) (persistedState, Block, error) {', + 'func produceBlockFromState(state persistedState, maxTransactions int, producedAt time.Time, chainID string) (persistedState, Block, error) {', +) +replace_exact( + store, + '\tif producedAt.IsZero() {\n\t\tproducedAt = time.Now().UTC()\n\t}\n\tblock := Block{', + '\trootState := state\n\trootState.Accounts = accounts\n\tstateRoot, err := stateRootFromState(chainID, rootState)\n\tif err != nil {\n\t\treturn state, Block{}, err\n\t}\n\n\tif producedAt.IsZero() {\n\t\tproducedAt = time.Now().UTC()\n\t}\n\tblock := Block{\n\t\tChainID: chainID,\n\t\tStateRoot: stateRoot,', +) +replace_exact( + store, + 'func importBlockIntoState(state persistedState, block Block) (persistedState, error) {\n\tstate = normalizeState(state)', + 'func importBlockIntoState(state persistedState, block Block, chainID string) (persistedState, error) {\n\tstate = normalizeState(state)\n\tif block.ChainID != chainID || block.StateRoot == "" {\n\t\treturn state, ErrInvalidBlock\n\t}', +) +replace_exact( + store, + '\t\tif err := envelope.ValidateStatic(); err != nil {', + '\t\tif err := envelope.ValidateForChain(chainID); err != nil {', + 1, +) +replace_exact( + store, + '\tsanitized := Block{\n\t\tHeight:', + '\trootState := state\n\trootState.Accounts = accounts\n\tstateRoot, err := stateRootFromState(chainID, rootState)\n\tif err != nil || stateRoot != block.StateRoot {\n\t\treturn state, ErrBlockInvariant\n\t}\n\n\tsanitized := Block{\n\t\tChainID: chainID,\n\t\tStateRoot: stateRoot,\n\t\tHeight:', +) +replace_exact( + store, + 'return consensus.BlockHash(block.Height, block.PreviousHash, block.ProducedAt, block.TransactionIDs)', + 'return consensus.BlockHash(block.ChainID, block.Height, block.PreviousHash, block.ProducedAt, block.StateRoot, block.TransactionIDs)', +) + +cs = "internal/ledger/consensus_state.go" +replace_exact( + cs, + '\tstate := s.snapshotLocked()\n\tnextState, err := recordProposalIntoState(state, proposal)', + '\tif err := proposal.ValidateForChain(s.chainID); err != nil {\n\t\treturn err\n\t}\n\tstate := s.snapshotLocked()\n\tnextState, err := recordProposalIntoState(state, proposal)', +) +replace_exact( + cs, + '\tstate := s.snapshotLocked()\n\tnextState, tally, certificate, err := recordVoteIntoState(state, vote)', + '\tif err := vote.ValidateForChain(s.chainID); err != nil {\n\t\treturn VoteTally{}, nil, err\n\t}\n\tstate := s.snapshotLocked()\n\tnextState, tally, certificate, err := recordVoteIntoState(state, vote)', +) + +snap = "internal/ledger/snapshot_security.go" +replace_exact( + snap, + '"time"\n\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"', + '"time"\n\n\t"github.com/zephyr-chain/zephyr-chain/internal/consensus"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"', +) + +Path("internal/ledger/peer_snapshot_restore.go").write_text( + '''package ledger\n\nimport "time"\n\n// RestoreFromPeerSnapshot is intentionally disabled. Peer snapshots require\n// quorum proofs from the locally trusted validator set.\nfunc (s *Store) RestoreFromPeerSnapshot(snapshot Snapshot, now time.Time) error {\n\treturn ErrSnapshotQuorumRequired\n}\n''' +) + +server = "internal/api/server.go" +replace_exact( + server, + '"github.com/zephyr-chain/zephyr-chain/internal/ledger"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', + '"github.com/zephyr-chain/zephyr-chain/internal/ledger"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', +) +replace_exact(server, 'type Config struct {\n\tDataDir', 'type Config struct {\n\tChainID string\n\tDataDir') +replace_exact( + server, + '\treturn Config{\n\t\tDataDir:', + '\treturn Config{\n\t\tChainID: protocol.DefaultChainID,\n\t\tDataDir:', +) +replace_exact( + server, + 'type StatusResponse struct {\n\tNodeID', + 'type StatusResponse struct {\n\tChainID string `json:"chainId"`\n\tNodeID', +) +replace_exact(server, '\tstore, err := ledger.NewStore(config.DataDir)', '\tstore, err := ledger.NewStoreWithChainID(config.DataDir, config.ChainID)') +replace_exact( + server, + '\tresponse := StatusResponse{\n\t\tNodeID:', + '\tresponse := StatusResponse{\n\t\tChainID: s.config.ChainID,\n\t\tNodeID:', +) +replace_exact( + server, + '\tconfig.ValidatorAddress = strings.TrimSpace(config.ValidatorAddress)', + '\tconfig.ChainID = protocol.ConfiguredChainID(config.ChainID)\n\tconfig.ValidatorAddress = strings.TrimSpace(config.ValidatorAddress)', +) +if "request.ValidateStatic()" in Path(server).read_text(): + replace_exact(server, "request.ValidateStatic()", "request.ValidateForChain(s.config.ChainID)", 1) +replace_exact( + server, + 'func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, SnapshotResponse{Snapshot: s.ledger.Snapshot()})\n}', + 'func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tif err := s.validatePeerRequest(r); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsnapshot, err := s.signedSnapshot()\n\tif err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\twriteJSON(w, http.StatusOK, SnapshotResponse{Snapshot: snapshot})\n}', +) +replace_exact( + server, + 'errors.Is(err, tx.ErrInvalidSignature),', + 'errors.Is(err, tx.ErrInvalidSignature),\n\t\terrors.Is(err, tx.ErrNonCanonicalSignature),\n\t\terrors.Is(err, tx.ErrInvalidChainID),\n\t\terrors.Is(err, tx.ErrInvalidDomain),', +) +replace_exact( + server, + 'errors.Is(err, consensus.ErrInvalidSignature),', + 'errors.Is(err, consensus.ErrInvalidSignature),\n\t\terrors.Is(err, consensus.ErrInvalidChainID),\n\t\terrors.Is(err, consensus.ErrInvalidDomain),\n\t\terrors.Is(err, consensus.ErrInvalidStateRoot),', +) +replace_exact( + server, + 'errors.Is(err, errTransportIdentityValidatorMismatch):', + 'errors.Is(err, errTransportIdentityValidatorMismatch),\n\t\terrors.Is(err, errTransportIdentityChainMismatch),\n\t\terrors.Is(err, errMissingRequestProof),\n\t\terrors.Is(err, errInvalidRequestProof),\n\t\terrors.Is(err, errRequestChainMismatch),\n\t\terrors.Is(err, errRequestDomainMismatch),\n\t\terrors.Is(err, errRequestTimestamp),\n\t\terrors.Is(err, ledger.ErrInvalidSnapshot),\n\t\terrors.Is(err, ledger.ErrSnapshotChainMismatch),\n\t\terrors.Is(err, ledger.ErrSnapshotProofInvalid),\n\t\terrors.Is(err, ledger.ErrInvalidStateRoot):', +) +replace_exact( + server, + 'case errors.Is(err, errPeerIdentityRequired),\n\t\terrors.Is(err, errPeerValidatorNotAllowed):', + 'case errors.Is(err, errPeerIdentityRequired),\n\t\terrors.Is(err, errPeerValidatorNotAllowed),\n\t\terrors.Is(err, errRequestReplay),\n\t\terrors.Is(err, errRequestReplayStoreFull),\n\t\terrors.Is(err, ledger.ErrSnapshotQuorumRequired),\n\t\terrors.Is(err, errSnapshotSignerRequired):', +) + +api_consensus = "internal/api/consensus_api.go" +replace_exact( + api_consensus, + '\tif request.ProposedAt.IsZero() {\n\t\trequest.ProposedAt = time.Now().UTC()\n\t}\n\n\tsourceNode :=', + '\tif request.ProposedAt.IsZero() {\n\t\trequest.ProposedAt = time.Now().UTC()\n\t}\n\tif err := request.ValidateForChain(s.config.ChainID); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsourceNode :=', +) +replace_exact( + api_consensus, + '\tif request.VotedAt.IsZero() {\n\t\trequest.VotedAt = time.Now().UTC()\n\t}\n\n\tsourceNode :=', + '\tif request.VotedAt.IsZero() {\n\t\trequest.VotedAt = time.Now().UTC()\n\t}\n\tif err := request.ValidateForChain(s.config.ChainID); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsourceNode :=', +) + +automation = "internal/api/consensus_automation.go" +replace_exact( + automation, + '\t\tproposal.PreviousHash = previousProposal.PreviousHash\n\t\tproposal.ProducedAt', + '\t\tproposal.PreviousHash = previousProposal.PreviousHash\n\t\tproposal.StateRoot = previousProposal.StateRoot\n\t\tproposal.ProducedAt', +) +replace_exact( + automation, + '\t\tproposal.PreviousHash = block.PreviousHash\n\t\tproposal.ProducedAt', + '\t\tproposal.PreviousHash = block.PreviousHash\n\t\tproposal.StateRoot = block.StateRoot\n\t\tproposal.ProducedAt', +) + +peer_sync = "internal/api/peer_sync.go" +replace_regex( + peer_sync, + r'func \(s \*Server\) restoreSnapshotFromPeer\(peerURL string, reason string\) \(peerSnapshotRestoreResult, error\) \{.*?\n\}\n\nfunc \(s \*Server\) broadcastTransaction', + '''func (s *Server) restoreSnapshotFromPeer(peerURL string, reason string) (peerSnapshotRestoreResult, error) { +\ttrusted := s.ledger.ValidatorSet() +\tif len(trusted.Validators) == 0 { +\t\treturn peerSnapshotRestoreResult{}, ledger.ErrSnapshotQuorumRequired +\t} + +\tsnapshot, err := s.fetchPeerSnapshot(peerURL) +\tif err != nil { +\t\treturn peerSnapshotRestoreResult{}, err +\t} +\tif uint64(len(snapshot.Blocks)) < s.ledger.Status().Height { +\t\treturn peerSnapshotRestoreResult{Reason: reason}, nil +\t} +\tif err := ledger.ValidateSnapshotCommittedState(snapshot, s.config.ChainID); err != nil { +\t\treturn peerSnapshotRestoreResult{}, err +\t} + +\tproofs := []ledger.SnapshotProof{snapshot.Proof} +\tfor _, candidateURL := range s.config.PeerURLs { +\t\tif candidateURL == peerURL { +\t\t\tcontinue +\t\t} +\t\tother, fetchErr := s.fetchPeerSnapshot(candidateURL) +\t\tif fetchErr != nil || len(other.Blocks) == 0 { +\t\t\tcontinue +\t\t} +\t\totherLatest := other.Blocks[len(other.Blocks)-1] +\t\tlatest := snapshot.Blocks[len(snapshot.Blocks)-1] +\t\tif otherLatest.Height != latest.Height || otherLatest.Hash != latest.Hash || otherLatest.StateRoot != latest.StateRoot || other.ValidatorSnapshot.Version != snapshot.ValidatorSnapshot.Version { +\t\t\tcontinue +\t\t} +\t\tproofs = append(proofs, other.Proof) +\t} + +\tnow := time.Now().UTC() +\tif err := s.ledger.RestoreQuorumSnapshot(snapshot, s.config.ChainID, proofs, trusted, now); err != nil { +\t\treturn peerSnapshotRestoreResult{}, err +\t} +\ts.recordSnapshotRestore(peerURL, snapshot, now) +\tresult := peerSnapshotRestoreResult{ +\t\tApplied: true, +\t\tRestoredAt: now, +\t\tHeight: uint64(len(snapshot.Blocks)), +\t\tReason: reason, +\t} +\tif len(snapshot.Blocks) > 0 { +\t\tresult.BlockHash = snapshot.Blocks[len(snapshot.Blocks)-1].Hash +\t} +\treturn result, nil +} + +func (s *Server) broadcastTransaction''', +) + +main = "cmd/node/main.go" +replace_exact( + main, + '\tif nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {', + '\tif chainID := os.Getenv("ZEPHYR_CHAIN_ID"); chainID != "" {\n\t\tconfig.ChainID = chainID\n\t}\n\tif nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {', +) +replace_exact(main, '"zephyr node %s listening on %s', '"zephyr node %s on chain %s listening on %s') +replace_exact(main, '\t\tconfig.NodeID,\n\t\taddr,', '\t\tconfig.NodeID,\n\t\tconfig.ChainID,\n\t\taddr,') From 7327e34db2e21073b812f219f706ebb3202378e7 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:11:41 +0200 Subject: [PATCH 23/88] run protocol integration from repository script --- .../workflows/protocol-backend-refresh.yml | 172 +----------------- 1 file changed, 2 insertions(+), 170 deletions(-) diff --git a/.github/workflows/protocol-backend-refresh.yml b/.github/workflows/protocol-backend-refresh.yml index 8629fca9..3bd9d264 100644 --- a/.github/workflows/protocol-backend-refresh.yml +++ b/.github/workflows/protocol-backend-refresh.yml @@ -16,182 +16,14 @@ jobs: with: ref: chatgpt/protocol-hardening fetch-depth: 0 - - - name: Integrate chain identity, state roots, and snapshot quorum - shell: bash + - name: Integrate protocol backend run: | - python - <<'PY' - from pathlib import Path - import re - - def replace_exact(path, old, new, expected=1): - p = Path(path) - s = p.read_text() - count = s.count(old) - if count != expected: - raise SystemExit(f'{path}: expected {expected} occurrences of {old!r}, found {count}') - p.write_text(s.replace(old, new)) - - def replace_regex(path, pattern, replacement, expected=1): - p = Path(path) - s = p.read_text() - s2, count = re.subn(pattern, replacement, s, flags=re.S) - if count != expected: - raise SystemExit(f'{path}: expected {expected} regex replacements for {pattern!r}, found {count}') - p.write_text(s2) - - store = 'internal/ledger/store.go' - replace_exact(store, - '"github.com/zephyr-chain/zephyr-chain/internal/dpos"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', - '"github.com/zephyr-chain/zephyr-chain/internal/dpos"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"') - replace_exact(store, - 'type Block struct {\n\tHeight uint64 `json:"height"`', - 'type Block struct {\n\tChainID string `json:"chainId"`\n\tHeight uint64 `json:"height"`') - replace_exact(store, - '\tPreviousHash string `json:"previousHash"`\n\tProducedAt', - '\tPreviousHash string `json:"previousHash"`\n\tStateRoot string `json:"stateRoot"`\n\tProducedAt') - replace_exact(store, - '\tPeerSyncIncidents []PeerSyncIncident `json:"peerSyncIncidents"`\n}', - '\tPeerSyncIncidents []PeerSyncIncident `json:"peerSyncIncidents"`\n\tProof SnapshotProof `json:"proof"`\n}', 1) - replace_exact(store, - 'type Store struct {\n\tmu sync.RWMutex', - 'type Store struct {\n\tmu sync.RWMutex\n\tchainID string') - replace_exact(store, - 'func NewStore(dataDir string) (*Store, error) {\n\tif dataDir == "" {', - 'func NewStore(dataDir string) (*Store, error) {\n\treturn NewStoreWithChainID(dataDir, protocol.DefaultChainID)\n}\n\nfunc NewStoreWithChainID(dataDir string, chainID string) (*Store, error) {\n\tchainID = protocol.ConfiguredChainID(chainID)\n\tif err := protocol.ValidateChainID(chainID); err != nil {\n\t\treturn nil, err\n\t}\n\tif dataDir == "" {') - replace_exact(store, - '\tstore := &Store{\n\t\tdataDir:', - '\tstore := &Store{\n\t\tchainID: chainID,\n\t\tdataDir:') - replace_exact(store, - '\t_, block, err := produceBlockFromState(state, maxTransactions, producedAt)', - '\t_, block, err := produceBlockFromState(state, maxTransactions, producedAt, s.chainID)') - replace_exact(store, - '\t\tnextState, block, err = produceCertifiedBlockFromState(state, producedAt)', - '\t\tnextState, block, err = produceCertifiedBlockFromState(state, producedAt, s.chainID)') - replace_exact(store, - '\t\tnextState, block, err = produceBlockFromState(state, maxTransactions, producedAt)', - '\t\tnextState, block, err = produceBlockFromState(state, maxTransactions, producedAt, s.chainID)') - replace_exact(store, - '\tnextState, err := importBlockIntoState(state, block)', - '\tnextState, err := importBlockIntoState(state, block, s.chainID)') - replace_exact(store, - 'func produceBlockFromState(state persistedState, maxTransactions int, producedAt time.Time) (persistedState, Block, error) {', - 'func produceBlockFromState(state persistedState, maxTransactions int, producedAt time.Time, chainID string) (persistedState, Block, error) {') - replace_exact(store, - '\tif producedAt.IsZero() {\n\t\tproducedAt = time.Now().UTC()\n\t}\n\tblock := Block{', - '\trootState := state\n\trootState.Accounts = accounts\n\tstateRoot, err := stateRootFromState(chainID, rootState)\n\tif err != nil {\n\t\treturn state, Block{}, err\n\t}\n\n\tif producedAt.IsZero() {\n\t\tproducedAt = time.Now().UTC()\n\t}\n\tblock := Block{\n\t\tChainID: chainID,\n\t\tStateRoot: stateRoot,') - replace_exact(store, - 'func importBlockIntoState(state persistedState, block Block) (persistedState, error) {\n\tstate = normalizeState(state)', - 'func importBlockIntoState(state persistedState, block Block, chainID string) (persistedState, error) {\n\tstate = normalizeState(state)\n\tif block.ChainID != chainID || block.StateRoot == "" {\n\t\treturn state, ErrInvalidBlock\n\t}') - replace_exact(store, - '\t\tif err := envelope.ValidateStatic(); err != nil {', - '\t\tif err := envelope.ValidateForChain(chainID); err != nil {', 1) - replace_exact(store, - '\tsanitized := Block{\n\t\tHeight:', - '\trootState := state\n\trootState.Accounts = accounts\n\tstateRoot, err := stateRootFromState(chainID, rootState)\n\tif err != nil || stateRoot != block.StateRoot {\n\t\treturn state, ErrBlockInvariant\n\t}\n\n\tsanitized := Block{\n\t\tChainID: chainID,\n\t\tStateRoot: stateRoot,\n\t\tHeight:') - replace_exact(store, - 'return consensus.BlockHash(block.Height, block.PreviousHash, block.ProducedAt, block.TransactionIDs)', - 'return consensus.BlockHash(block.ChainID, block.Height, block.PreviousHash, block.ProducedAt, block.StateRoot, block.TransactionIDs)') - replace_exact(store, - '\t\tPeerSyncIncidents: clonePeerSyncIncidents(state.PeerSyncIncidents),\n\t}', - '\t\tPeerSyncIncidents: clonePeerSyncIncidents(state.PeerSyncIncidents),\n\t}', 1) - - cs = 'internal/ledger/consensus_state.go' - replace_exact(cs, - '\tstate := s.snapshotLocked()\n\tnextState, err := recordProposalIntoState(state, proposal)', - '\tif err := proposal.ValidateForChain(s.chainID); err != nil {\n\t\treturn err\n\t}\n\tstate := s.snapshotLocked()\n\tnextState, err := recordProposalIntoState(state, proposal)') - replace_exact(cs, - '\tstate := s.snapshotLocked()\n\tnextState, tally, certificate, err := recordVoteIntoState(state, vote)', - '\tif err := vote.ValidateForChain(s.chainID); err != nil {\n\t\treturn VoteTally{}, nil, err\n\t}\n\tstate := s.snapshotLocked()\n\tnextState, tally, certificate, err := recordVoteIntoState(state, vote)') - - snap = 'internal/ledger/snapshot_security.go' - replace_exact(snap, - '"time"\n\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"', - '"time"\n\n\t"github.com/zephyr-chain/zephyr-chain/internal/consensus"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"') - - old_restore = 'internal/ledger/peer_snapshot_restore.go' - Path(old_restore).write_text('''package ledger\n\nimport "time"\n\n// RestoreFromPeerSnapshot is intentionally disabled. Peer snapshots require\n// quorum proofs from the locally trusted validator set.\nfunc (s *Store) RestoreFromPeerSnapshot(snapshot Snapshot, now time.Time) error {\n\treturn ErrSnapshotQuorumRequired\n}\n''') - - server = 'internal/api/server.go' - replace_exact(server, - '"github.com/zephyr-chain/zephyr-chain/internal/ledger"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"', - '"github.com/zephyr-chain/zephyr-chain/internal/ledger"\n\t"github.com/zephyr-chain/zephyr-chain/internal/protocol"\n\t"github.com/zephyr-chain/zephyr-chain/internal/tx"') - replace_exact(server, - 'type Config struct {\n\tDataDir', - 'type Config struct {\n\tChainID string\n\tDataDir') - replace_exact(server, - '\treturn Config{\n\t\tDataDir:', - '\treturn Config{\n\t\tChainID: protocol.DefaultChainID,\n\t\tDataDir:') - replace_exact(server, - 'type StatusResponse struct {\n\tNodeID', - 'type StatusResponse struct {\n\tChainID string `json:"chainId"`\n\tNodeID') - replace_exact(server, - '\tstore, err := ledger.NewStore(config.DataDir)', - '\tstore, err := ledger.NewStoreWithChainID(config.DataDir, config.ChainID)') - replace_exact(server, - '\tresponse := StatusResponse{\n\t\tNodeID:', - '\tresponse := StatusResponse{\n\t\tChainID: s.config.ChainID,\n\t\tNodeID:') - replace_exact(server, - '\tconfig.ValidatorAddress = strings.TrimSpace(config.ValidatorAddress)', - '\tconfig.ChainID = protocol.ConfiguredChainID(config.ChainID)\n\tconfig.ValidatorAddress = strings.TrimSpace(config.ValidatorAddress)') - if 'request.ValidateStatic()' in Path(server).read_text(): - replace_exact(server, 'request.ValidateStatic()', 'request.ValidateForChain(s.config.ChainID)', 1) - replace_exact(server, - 'func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\twriteJSON(w, http.StatusOK, SnapshotResponse{Snapshot: s.ledger.Snapshot()})\n}', - 'func (s *Server) handleSnapshot(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != http.MethodGet {\n\t\tw.WriteHeader(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tif err := s.validatePeerRequest(r); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsnapshot, err := s.signedSnapshot()\n\tif err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\twriteJSON(w, http.StatusOK, SnapshotResponse{Snapshot: snapshot})\n}') - replace_exact(server, - 'errors.Is(err, tx.ErrInvalidSignature),', - 'errors.Is(err, tx.ErrInvalidSignature),\n\t\terrors.Is(err, tx.ErrNonCanonicalSignature),\n\t\terrors.Is(err, tx.ErrInvalidChainID),\n\t\terrors.Is(err, tx.ErrInvalidDomain),') - replace_exact(server, - 'errors.Is(err, consensus.ErrInvalidSignature),', - 'errors.Is(err, consensus.ErrInvalidSignature),\n\t\terrors.Is(err, consensus.ErrInvalidChainID),\n\t\terrors.Is(err, consensus.ErrInvalidDomain),\n\t\terrors.Is(err, consensus.ErrInvalidStateRoot),') - replace_exact(server, - 'errors.Is(err, errTransportIdentityValidatorMismatch):', - 'errors.Is(err, errTransportIdentityValidatorMismatch),\n\t\terrors.Is(err, errTransportIdentityChainMismatch),\n\t\terrors.Is(err, errMissingRequestProof),\n\t\terrors.Is(err, errInvalidRequestProof),\n\t\terrors.Is(err, errRequestChainMismatch),\n\t\terrors.Is(err, errRequestDomainMismatch),\n\t\terrors.Is(err, errRequestTimestamp),\n\t\terrors.Is(err, ledger.ErrInvalidSnapshot),\n\t\terrors.Is(err, ledger.ErrSnapshotChainMismatch),\n\t\terrors.Is(err, ledger.ErrSnapshotProofInvalid),\n\t\terrors.Is(err, ledger.ErrInvalidStateRoot):') - replace_exact(server, - 'case errors.Is(err, errPeerIdentityRequired),\n\t\terrors.Is(err, errPeerValidatorNotAllowed):', - 'case errors.Is(err, errPeerIdentityRequired),\n\t\terrors.Is(err, errPeerValidatorNotAllowed),\n\t\terrors.Is(err, errRequestReplay),\n\t\terrors.Is(err, errRequestReplayStoreFull),\n\t\terrors.Is(err, ledger.ErrSnapshotQuorumRequired),\n\t\terrors.Is(err, errSnapshotSignerRequired):') - - api_consensus = 'internal/api/consensus_api.go' - replace_exact(api_consensus, - '\tif request.ProposedAt.IsZero() {\n\t\trequest.ProposedAt = time.Now().UTC()\n\t}\n\n\tsourceNode :=', - '\tif request.ProposedAt.IsZero() {\n\t\trequest.ProposedAt = time.Now().UTC()\n\t}\n\tif err := request.ValidateForChain(s.config.ChainID); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsourceNode :=') - replace_exact(api_consensus, - '\tif request.VotedAt.IsZero() {\n\t\trequest.VotedAt = time.Now().UTC()\n\t}\n\n\tsourceNode :=', - '\tif request.VotedAt.IsZero() {\n\t\trequest.VotedAt = time.Now().UTC()\n\t}\n\tif err := request.ValidateForChain(s.config.ChainID); err != nil {\n\t\twriteJSON(w, statusForError(err), map[string]string{"error": err.Error()})\n\t\treturn\n\t}\n\n\tsourceNode :=') - - automation = 'internal/api/consensus_automation.go' - replace_exact(automation, - '\t\tproposal.PreviousHash = previousProposal.PreviousHash\n\t\tproposal.ProducedAt', - '\t\tproposal.PreviousHash = previousProposal.PreviousHash\n\t\tproposal.StateRoot = previousProposal.StateRoot\n\t\tproposal.ProducedAt') - replace_exact(automation, - '\t\tproposal.PreviousHash = block.PreviousHash\n\t\tproposal.ProducedAt', - '\t\tproposal.PreviousHash = block.PreviousHash\n\t\tproposal.StateRoot = block.StateRoot\n\t\tproposal.ProducedAt') - - peer_sync = 'internal/api/peer_sync.go' - replace_regex(peer_sync, - r'func \(s \*Server\) restoreSnapshotFromPeer\(peerURL string, reason string\) \(peerSnapshotRestoreResult, error\) \{.*?\n\}\n\nfunc \(s \*Server\) broadcastTransaction', - '''func (s *Server) restoreSnapshotFromPeer(peerURL string, reason string) (peerSnapshotRestoreResult, error) {\n\ttrusted := s.ledger.ValidatorSet()\n\tif len(trusted.Validators) == 0 {\n\t\treturn peerSnapshotRestoreResult{}, ledger.ErrSnapshotQuorumRequired\n\t}\n\n\tsnapshot, err := s.fetchPeerSnapshot(peerURL)\n\tif err != nil {\n\t\treturn peerSnapshotRestoreResult{}, err\n\t}\n\tif uint64(len(snapshot.Blocks)) < s.ledger.Status().Height {\n\t\treturn peerSnapshotRestoreResult{Reason: reason}, nil\n\t}\n\tif err := ledger.ValidateSnapshotCommittedState(snapshot, s.config.ChainID); err != nil {\n\t\treturn peerSnapshotRestoreResult{}, err\n\t}\n\n\tproofs := []ledger.SnapshotProof{snapshot.Proof}\n\tfor _, candidateURL := range s.config.PeerURLs {\n\t\tif candidateURL == peerURL {\n\t\t\tcontinue\n\t\t}\n\t\tother, fetchErr := s.fetchPeerSnapshot(candidateURL)\n\t\tif fetchErr != nil || len(other.Blocks) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\totherLatest := other.Blocks[len(other.Blocks)-1]\n\t\tlatest := snapshot.Blocks[len(snapshot.Blocks)-1]\n\t\tif otherLatest.Height != latest.Height || otherLatest.Hash != latest.Hash || otherLatest.StateRoot != latest.StateRoot || other.ValidatorSnapshot.Version != snapshot.ValidatorSnapshot.Version {\n\t\t\tcontinue\n\t\t}\n\t\tproofs = append(proofs, other.Proof)\n\t}\n\n\tnow := time.Now().UTC()\n\tif err := s.ledger.RestoreQuorumSnapshot(snapshot, s.config.ChainID, proofs, trusted, now); err != nil {\n\t\treturn peerSnapshotRestoreResult{}, err\n\t}\n\ts.recordSnapshotRestore(peerURL, snapshot, now)\n\tresult := peerSnapshotRestoreResult{\n\t\tApplied: true,\n\t\tRestoredAt: now,\n\t\tHeight: uint64(len(snapshot.Blocks)),\n\t\tReason: reason,\n\t}\n\tif len(snapshot.Blocks) > 0 {\n\t\tresult.BlockHash = snapshot.Blocks[len(snapshot.Blocks)-1].Hash\n\t}\n\treturn result, nil\n}\n\nfunc (s *Server) broadcastTransaction''') - - main = 'cmd/node/main.go' - replace_exact(main, - '\tif nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {', - '\tif chainID := os.Getenv("ZEPHYR_CHAIN_ID"); chainID != "" {\n\t\tconfig.ChainID = chainID\n\t}\n\tif nodeID := os.Getenv("ZEPHYR_NODE_ID"); nodeID != "" {') - replace_exact(main, - '"zephyr node %s listening on %s', - '"zephyr node %s on chain %s listening on %s') - replace_exact(main, - '\t\tconfig.NodeID,\n\t\taddr,', - '\t\tconfig.NodeID,\n\t\tconfig.ChainID,\n\t\taddr,') - PY - + python scripts/protocol_backend_refresh.py gofmt -w internal/protocol internal/tx internal/consensus internal/ledger internal/api cmd/node - - name: Verify backend run: | go vet ./... go test ./... - - name: Commit verified backend integration shell: bash run: | From 4461de1cbcecb5e0169afd58e01d37098f9b476b Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:12:01 +0200 Subject: [PATCH 24/88] add chain-bound transaction and node status types --- apps/wallet/src/types.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/wallet/src/types.ts b/apps/wallet/src/types.ts index 69decf44..4322aff9 100644 --- a/apps/wallet/src/types.ts +++ b/apps/wallet/src/types.ts @@ -32,11 +32,18 @@ export type TransactionDraft = { } export type SignedTransactionEnvelope = TransactionDraft & { + chainId: string + domain: 'zephyr/transaction/v1' payload: string publicKey: string signature: string } +export type NodeStatusResponse = { + chainId: string + nodeId: string +} + export type BroadcastResponse = { id: string accepted: boolean @@ -51,6 +58,7 @@ export type AccountView = { nonce: number nextNonce: number pendingTransactions: number + nonceExhausted?: boolean } export type AccountResponse = { From f8bc1e805b1a03538fe140bf555fb31501c29d68 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:12:34 +0200 Subject: [PATCH 25/88] read chain id from node status --- apps/wallet/src/lib/network.ts | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/apps/wallet/src/lib/network.ts b/apps/wallet/src/lib/network.ts index a1cb3daa..75a59909 100644 --- a/apps/wallet/src/lib/network.ts +++ b/apps/wallet/src/lib/network.ts @@ -4,6 +4,7 @@ import type { ApiError, BroadcastResponse, FaucetResponse, + NodeStatusResponse, SignedTransactionEnvelope } from '../types' @@ -12,6 +13,11 @@ export async function pingNode(apiBase: string): Promise { return response.ok } +export async function fetchNodeStatus(apiBase: string): Promise { + const response = await fetch(url(apiBase, '/v1/status')) + return readJSON(response) +} + export async function fetchAccount(apiBase: string, address: string): Promise { const response = await fetch(url(apiBase, `/v1/accounts/${encodeURIComponent(address)}`)) const payload = await readJSON(response) @@ -21,40 +27,29 @@ export async function fetchAccount(apiBase: string, address: string): Promise { const response = await fetch(url(apiBase, '/v1/dev/faucet'), { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address, amount }) }) - const payload = await readJSON(response) return payload.account } -export async function broadcastTransaction( - apiBase: string, - envelope: SignedTransactionEnvelope -): Promise { +export async function broadcastTransaction(apiBase: string, envelope: SignedTransactionEnvelope): Promise { const response = await fetch(url(apiBase, '/v1/transactions'), { method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(envelope) }) - return readJSON(response) } async function readJSON(response: Response): Promise { const raw = await response.text() const payload = raw ? (JSON.parse(raw) as T | ApiError) : null - if (!response.ok) { const message = payload && typeof payload === 'object' && 'error' in payload ? payload.error : undefined throw new Error(message || `Request failed with status ${response.status}`) } - return payload as T } From 797b3f4ad4305f87c3a4b67934c4828ff500b114 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:13:05 +0200 Subject: [PATCH 26/88] add wallet protocol integration script --- scripts/protocol_wallet_refresh.py | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 scripts/protocol_wallet_refresh.py diff --git a/scripts/protocol_wallet_refresh.py b/scripts/protocol_wallet_refresh.py new file mode 100644 index 00000000..91062152 --- /dev/null +++ b/scripts/protocol_wallet_refresh.py @@ -0,0 +1,65 @@ +from pathlib import Path + + +def replace_exact(path, old, new, expected=1): + p = Path(path) + s = p.read_text() + count = s.count(old) + if count != expected: + raise SystemExit(f"{path}: expected {expected} occurrences of {old!r}, found {count}") + p.write_text(s.replace(old, new)) + + +wallet = "apps/wallet/src/lib/wallet.ts" +replace_exact( + wallet, + "const MAX_KDF_ITERATIONS = 1_000_000\n", + "const MAX_KDF_ITERATIONS = 1_000_000\nconst TRANSACTION_DOMAIN = 'zephyr/transaction/v1' as const\nconst P256_ORDER = BigInt('0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551')\nconst P256_HALF_ORDER = P256_ORDER >> 1n\n", +) +replace_exact( + wallet, + "export async function signTransaction(\n account: StoredAccount,\n draft: TransactionDraft\n): Promise {\n await assertAccountIntegrity(account)\n", + "export async function signTransaction(\n account: StoredAccount,\n draft: TransactionDraft,\n chainId: string\n): Promise {\n await assertAccountIntegrity(account)\n chainId = chainId.trim()\n if (!/^[A-Za-z0-9._-]{1,64}$/.test(chainId)) {\n throw new Error('Connect to a node with a valid Zephyr chain ID before signing')\n }\n", +) +replace_exact( + wallet, + " const payload = canonicalize({\n from: draft.from,\n to: draft.to,\n amount: draft.amount,\n nonce: draft.nonce,\n memo: draft.memo\n })", + " const payload = canonicalize({\n amount: draft.amount,\n chainId,\n domain: TRANSACTION_DOMAIN,\n from: draft.from,\n memo: draft.memo,\n nonce: draft.nonce,\n to: draft.to\n })", +) +replace_exact( + wallet, + " signature: bytesToBase64(new Uint8Array(signature))\n }", + " chainId,\n domain: TRANSACTION_DOMAIN,\n signature: bytesToBase64(normalizeP256Signature(new Uint8Array(signature)))\n }", +) +replace_exact( + wallet, + "function bytesToHex(bytes: Uint8Array): string {", + "function normalizeP256Signature(signature: Uint8Array): Uint8Array {\n if (signature.length !== 64) {\n throw new Error('Browser returned an invalid P-256 signature')\n }\n const r = bytesToBigInt(signature.slice(0, 32))\n let s = bytesToBigInt(signature.slice(32))\n if (s > P256_HALF_ORDER) {\n s = P256_ORDER - s\n }\n const normalized = new Uint8Array(64)\n normalized.set(bigIntTo32Bytes(r), 0)\n normalized.set(bigIntTo32Bytes(s), 32)\n return normalized\n}\n\nfunction bytesToBigInt(bytes: Uint8Array): bigint {\n const hex = bytesToHex(bytes) || '0'\n return BigInt(`0x${hex}`)\n}\n\nfunction bigIntTo32Bytes(value: bigint): Uint8Array {\n const hex = value.toString(16).padStart(64, '0')\n if (hex.length > 64) {\n throw new Error('P-256 signature integer is out of range')\n }\n const bytes = new Uint8Array(32)\n for (let index = 0; index < 32; index += 1) {\n bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)\n }\n return bytes\n}\n\nfunction bytesToHex(bytes: Uint8Array): string {", +) + +app = "apps/wallet/src/App.vue" +replace_exact( + app, + "import { broadcastTransaction, fetchAccount, fundAccount, pingNode } from './lib/network'", + "import { broadcastTransaction, fetchAccount, fetchNodeStatus, fundAccount, pingNode } from './lib/network'", +) +replace_exact( + app, + "const networkHealthy = ref(null)\n", + "const networkHealthy = ref(null)\nconst chainId = ref('')\n", +) +replace_exact( + app, + "async function refreshHealth() {\n try {\n networkHealthy.value = await pingNode(apiBase.value)\n } catch {\n networkHealthy.value = false\n }\n}", + "async function refreshHealth() {\n try {\n networkHealthy.value = await pingNode(apiBase.value)\n if (!networkHealthy.value) {\n chainId.value = ''\n return\n }\n const status = await fetchNodeStatus(apiBase.value)\n chainId.value = status.chainId\n } catch {\n networkHealthy.value = false\n chainId.value = ''\n }\n}", +) +replace_exact( + app, + " const envelope = await signTransaction(account.value, form.value)", + " if (!chainId.value) {\n throw new Error('Node chain identity is unavailable; refresh the node connection before signing')\n }\n const envelope = await signTransaction(account.value, form.value, chainId.value)", +) +replace_exact( + app, + " {{ balancePill }}", + " Chain: {{ chainId || 'unavailable' }}\n {{ balancePill }}", +) From d2d36cded47a6f539ba4a7466a3986e9f3554fc3 Mon Sep 17 00:00:00 2001 From: the-code-learner <142033899+the-code-learner@users.noreply.github.com> Date: Tue, 18 Aug 2026 16:13:23 +0200 Subject: [PATCH 27/88] repurpose temporary workflow for wallet protocol verification --- .github/workflows/fix-protocol-refresh.yml | 36 +++++++++++++--------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/.github/workflows/fix-protocol-refresh.yml b/.github/workflows/fix-protocol-refresh.yml index b1c4abc9..ebab0b37 100644 --- a/.github/workflows/fix-protocol-refresh.yml +++ b/.github/workflows/fix-protocol-refresh.yml @@ -1,4 +1,4 @@ -name: Fix protocol refresh selector +name: Protocol wallet refresh on: pull_request: @@ -7,28 +7,34 @@ permissions: contents: write jobs: - fix: + wallet: if: github.actor != 'github-actions[bot]' runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 with: ref: chatgpt/protocol-hardening - - name: Fix snapshot selector + fetch-depth: 0 + - uses: actions/setup-node@v5 + with: + node-version: 24 + - name: Integrate wallet protocol + run: python scripts/protocol_wallet_refresh.py + - name: Verify wallet + working-directory: apps/wallet + run: | + npm ci + npm audit --audit-level=high + npm run build + - name: Commit verified wallet integration shell: bash run: | - python - <<'PY' - from pathlib import Path - p = Path('.github/workflows/protocol-backend-refresh.yml') - s = p.read_text() - old = """ replace_exact(store,\n '\\tPeerSyncIncidents []PeerSyncIncident `json:\"peerSyncIncidents\"`\\n}',\n '\\tPeerSyncIncidents []PeerSyncIncident `json:\"peerSyncIncidents\"`\\n\\tProof SnapshotProof `json:\"proof\"`\\n}', 1)""" - new = """ replace_regex(store,\n r'(type Snapshot struct \\{.*?\\tPeerSyncIncidents \\[\\]PeerSyncIncident `json:\"peerSyncIncidents\"`\\n)\\}',\n r'\\1\\tProof SnapshotProof `json:\"proof\"`\\n}', 1)""" - if old not in s: - raise SystemExit('snapshot selector block not found') - p.write_text(s.replace(old, new)) - PY git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add .github/workflows/protocol-backend-refresh.yml - git commit -m "fix protocol refresh snapshot selector" + git add apps/wallet + if git diff --cached --quiet; then + echo "No wallet integration changes to commit." + exit 0 + fi + git commit -m "bind wallet signatures to node chain identity" git push origin HEAD:chatgpt/protocol-hardening From 71fe26004781c802b251f9927edd60a247acb2a8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 14:13:48 +0000 Subject: [PATCH 28/88] bind wallet signatures to node chain identity --- apps/wallet/src/App.vue | 16 +++++++++-- apps/wallet/src/lib/wallet.ts | 54 +++++++++++++++++++++++++++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/apps/wallet/src/App.vue b/apps/wallet/src/App.vue index 21d409c6..caf9fe45 100644 --- a/apps/wallet/src/App.vue +++ b/apps/wallet/src/App.vue @@ -1,6 +1,6 @@