mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
proof satisfy RequireZKProofs Two defects. 1. Block.verifyCredential compared cred.ExpirationDate against time.Now() inside block verification. The verdict therefore depended on when a node happened to verify: the same block is valid before the expiry instant and invalid after, so two validators either side of it disagree. Worse, it is unconditional on replay — a node bootstrapping the chain rejects every historical block whose credentials have since expired, which after enough time is all of them. Proven: a block timestamped 48h ago carrying a credential that was live then failed verification. Now compared against b.BlockTimestamp, the one clock every node agrees on, already bounded against the parent and local time earlier in Verify. 2. VerifyCredential gated the ZK requirement on the presence of the thing it requires: `if vm.config.RequireZKProofs && cred.Proof != nil`. A credential carrying no proof object skipped the branch and returned (true, nil) on a chain configured to require proofs. Proven: VerifyCredential returned true for a proofless credential with RequireZKProofs set. The requirement now holds whenever it is configured. Tests: TestCredentialExpiryIsJudgedAtBlockTime, TestRequireZKProofsCannotBeSkippedByOmittingTheProof. Co-authored-by: Hanzo Dev <[email protected]>
236 lines
5.5 KiB
Go
236 lines
5.5 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package identityvm
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
)
|
|
|
|
// Block represents a block in the IdentityVM chain
|
|
type Block struct {
|
|
ParentID_ ids.ID `json:"parentId"`
|
|
BlockHeight uint64 `json:"height"`
|
|
BlockTimestamp int64 `json:"timestamp"`
|
|
Credentials []*Credential `json:"credentials"`
|
|
Revocations []*RevocationEntry `json:"revocations,omitempty"`
|
|
Identities []*Identity `json:"identities,omitempty"`
|
|
StateRoot []byte `json:"stateRoot"`
|
|
|
|
// Cached values
|
|
ID_ ids.ID
|
|
bytes []byte
|
|
status choices.Status
|
|
vm *VM
|
|
}
|
|
|
|
// ID returns the block ID
|
|
func (b *Block) ID() ids.ID {
|
|
if b.ID_ == ids.Empty {
|
|
b.ID_ = b.computeID()
|
|
}
|
|
return b.ID_
|
|
}
|
|
|
|
// computeID computes the block ID as the hash of the canonical ZAP wire.
|
|
// The wire commits to every block field, so identical logical blocks yield
|
|
// identical IDs and any field change moves the ID.
|
|
func (b *Block) computeID() ids.ID {
|
|
return ids.ID(sha256.Sum256(b.Bytes()))
|
|
}
|
|
|
|
// ParentID returns the parent block ID
|
|
func (b *Block) ParentID() ids.ID {
|
|
return b.ParentID_
|
|
}
|
|
|
|
// Parent is an alias for ParentID for compatibility
|
|
func (b *Block) Parent() ids.ID {
|
|
return b.ParentID_
|
|
}
|
|
|
|
// Height returns the block height
|
|
func (b *Block) Height() uint64 {
|
|
return b.BlockHeight
|
|
}
|
|
|
|
// Timestamp returns the block timestamp
|
|
func (b *Block) Timestamp() time.Time {
|
|
return time.Unix(b.BlockTimestamp, 0)
|
|
}
|
|
|
|
// Status returns the block status
|
|
func (b *Block) Status() uint8 {
|
|
return uint8(b.status)
|
|
}
|
|
|
|
// Verify verifies the block
|
|
func (b *Block) Verify(ctx context.Context) error {
|
|
// Verify height
|
|
if b.BlockHeight == 0 && b.ParentID_ != ids.Empty {
|
|
return errors.New("invalid genesis block")
|
|
}
|
|
|
|
// Verify timestamp is not too far in future
|
|
if b.BlockTimestamp > time.Now().Unix()+60 {
|
|
return errors.New("block timestamp too far in future")
|
|
}
|
|
|
|
// Verify parent exists and heights are consecutive
|
|
if b.BlockHeight > 0 {
|
|
parent, err := b.vm.GetBlock(ctx, b.ParentID_)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if b.BlockHeight != parent.Height()+1 {
|
|
return errors.New("non-consecutive block heights")
|
|
}
|
|
|
|
if b.BlockTimestamp < parent.Timestamp().Unix() {
|
|
return errors.New("block timestamp before parent")
|
|
}
|
|
}
|
|
|
|
// Verify all credentials
|
|
for _, cred := range b.Credentials {
|
|
if err := b.verifyCredential(cred); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Block) verifyCredential(cred *Credential) error {
|
|
// Verify issuer exists
|
|
_, err := b.vm.GetIssuer(cred.Issuer)
|
|
if err != nil && !b.vm.config.AllowSelfIssue {
|
|
return err
|
|
}
|
|
|
|
// Verify claims count
|
|
if len(cred.Claims) > b.vm.config.MaxClaims {
|
|
return errors.New("too many claims")
|
|
}
|
|
|
|
// The credential must be unexpired AS OF THIS BLOCK. Comparing against the
|
|
// wall clock would make the verdict depend on when a node happens to verify:
|
|
// the same block is valid before the expiry and invalid after it, and a node
|
|
// replaying history during bootstrap rejects every block whose credentials
|
|
// have since expired. The block timestamp is the only clock every node agrees
|
|
// on, and Verify has already bounded it against the parent and local time.
|
|
if time.Unix(b.BlockTimestamp, 0).After(cred.ExpirationDate) {
|
|
return errors.New("credential already expired")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Accept accepts the block
|
|
func (b *Block) Accept(ctx context.Context) error {
|
|
b.status = choices.Accepted
|
|
|
|
b.vm.mu.Lock()
|
|
defer b.vm.mu.Unlock()
|
|
|
|
// Update VM state
|
|
b.vm.lastAccepted = b
|
|
b.vm.lastAcceptedID = b.ID()
|
|
|
|
// Save last accepted
|
|
id := b.ID()
|
|
if err := b.vm.db.Put(lastAcceptedKey, id[:]); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save block
|
|
blockBytes := b.Bytes()
|
|
if blockBytes == nil {
|
|
return errors.New("failed to serialize block")
|
|
}
|
|
|
|
if err := b.vm.db.Put(id[:], blockBytes); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Process credentials
|
|
for _, cred := range b.Credentials {
|
|
b.vm.credentials[cred.ID] = cred
|
|
|
|
// Persist credential (native ZAP wire)
|
|
credBytes := marshalCredential(cred)
|
|
credKey := append(credentialPrefix, cred.ID[:]...)
|
|
b.vm.db.Put(credKey, credBytes)
|
|
|
|
// Remove from pending
|
|
for i, pending := range b.vm.pendingCreds {
|
|
if pending.ID == cred.ID {
|
|
b.vm.pendingCreds = append(b.vm.pendingCreds[:i], b.vm.pendingCreds[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Process revocations
|
|
for _, rev := range b.Revocations {
|
|
b.vm.revocations[rev.CredentialID] = rev
|
|
|
|
// Update credential status
|
|
if cred, ok := b.vm.credentials[rev.CredentialID]; ok {
|
|
cred.Status = CredentialRevoked
|
|
}
|
|
}
|
|
|
|
// Process new identities
|
|
for _, identity := range b.Identities {
|
|
b.vm.identities[identity.ID] = identity
|
|
}
|
|
|
|
// Remove from pending blocks
|
|
delete(b.vm.pendingBlocks, b.ID())
|
|
|
|
b.vm.log.Info("Block accepted",
|
|
log.Uint64("height", b.BlockHeight),
|
|
log.String("id", b.ID().String()),
|
|
log.Int("credentials", len(b.Credentials)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reject rejects the block
|
|
func (b *Block) Reject(ctx context.Context) error {
|
|
b.status = choices.Rejected
|
|
|
|
b.vm.mu.Lock()
|
|
defer b.vm.mu.Unlock()
|
|
|
|
// Remove from pending blocks
|
|
delete(b.vm.pendingBlocks, b.ID())
|
|
|
|
return nil
|
|
}
|
|
|
|
// Bytes returns the block bytes
|
|
func (b *Block) Bytes() []byte {
|
|
if b.bytes != nil {
|
|
return b.bytes
|
|
}
|
|
|
|
bytes, err := b.Marshal()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
b.bytes = bytes
|
|
return bytes
|
|
}
|