mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
I-Chain accepted credentials and never built a block from them. WaitForEvent answered with <-ctx.Done(), which never returns, so BuildBlock was never called and the chain could not leave genesis however many credentials were issued. IssueCredential now signals, and WaitForEvent is the shared latch. BuildBlock also built a block whatever was pending, including nothing. An empty block says nothing and still has to be voted on, and every other chain here refuses one, so it refuses too. Three tests built a block off an empty VM as setup rather than as a claim about empty blocks; they now issue a credential first, which exercises more than they did before. The liveness test fails without the signal, waiting its full timeout for a wake that never comes. Co-authored-by: Hanzo Dev <[email protected]>
800 lines
21 KiB
Go
800 lines
21 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"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/gorilla/rpc/v2"
|
|
grjson "github.com/gorilla/rpc/v2/json"
|
|
|
|
"github.com/luxfi/accel"
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"github.com/luxfi/database"
|
|
"github.com/luxfi/ids"
|
|
"github.com/luxfi/log"
|
|
"github.com/luxfi/node/vms/artifacts"
|
|
"github.com/luxfi/node/vms/types/fee"
|
|
"github.com/luxfi/runtime"
|
|
vmcore "github.com/luxfi/vm"
|
|
"github.com/luxfi/vm/chain"
|
|
)
|
|
|
|
const (
|
|
Name = "identityvm"
|
|
|
|
// Credential states
|
|
CredentialActive = "active"
|
|
CredentialRevoked = "revoked"
|
|
CredentialExpired = "expired"
|
|
CredentialPending = "pending"
|
|
|
|
// Default configuration
|
|
defaultCredentialTTL = 365 * 24 * time.Hour // 1 year
|
|
defaultMaxClaims = 100
|
|
)
|
|
|
|
var (
|
|
_ chain.ChainVM = (*VM)(nil)
|
|
|
|
lastAcceptedKey = []byte("lastAccepted")
|
|
identityPrefix = []byte("id:")
|
|
credentialPrefix = []byte("cred:")
|
|
issuerPrefix = []byte("issuer:")
|
|
revocationPrefix = []byte("revoke:")
|
|
|
|
errUnknownIdentity = errors.New("unknown identity")
|
|
errUnknownCredential = errors.New("unknown credential")
|
|
errCredentialRevoked = errors.New("credential revoked")
|
|
errCredentialExpired = errors.New("credential expired")
|
|
errNotIssuer = errors.New("not authorized issuer")
|
|
errNothingToBuild = errors.New("no credentials to build a block from")
|
|
errInvalidProof = errors.New("invalid zero-knowledge proof")
|
|
)
|
|
|
|
// Config holds IdentityVM configuration
|
|
type Config struct {
|
|
CredentialTTL int64 `json:"credentialTTL"` // Seconds
|
|
MaxClaims int `json:"maxClaims"`
|
|
TrustedIssuers []string `json:"trustedIssuers"`
|
|
AllowSelfIssue bool `json:"allowSelfIssue"`
|
|
RequireZKProofs bool `json:"requireZKProofs"`
|
|
}
|
|
|
|
// Identity represents a decentralized identity
|
|
type Identity struct {
|
|
ID ids.ID `json:"id"`
|
|
DID string `json:"did"` // Decentralized Identifier (e.g., did:lux:xyz)
|
|
PublicKey []byte `json:"publicKey"` // Primary public key
|
|
Controllers []ids.ID `json:"controllers"` // Controlling identities
|
|
Services []ServiceEndpoint `json:"services"`
|
|
Created time.Time `json:"created"`
|
|
Updated time.Time `json:"updated"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
// ServiceEndpoint represents a service associated with an identity
|
|
type ServiceEndpoint struct {
|
|
ID string `json:"id"`
|
|
Type string `json:"type"`
|
|
ServiceEndpoint string `json:"serviceEndpoint"`
|
|
}
|
|
|
|
// Credential represents a verifiable credential
|
|
type Credential struct {
|
|
ID ids.ID `json:"id"`
|
|
Type []string `json:"type"`
|
|
Issuer ids.ID `json:"issuer"`
|
|
Subject ids.ID `json:"subject"`
|
|
IssuanceDate time.Time `json:"issuanceDate"`
|
|
ExpirationDate time.Time `json:"expirationDate"`
|
|
Claims map[string]interface{} `json:"claims"`
|
|
Proof *CredentialProof `json:"proof,omitempty"`
|
|
Status string `json:"status"`
|
|
RevocationIndex uint64 `json:"revocationIndex,omitempty"`
|
|
}
|
|
|
|
// CredentialProof represents a proof for a credential
|
|
type CredentialProof struct {
|
|
Type string `json:"type"`
|
|
Created string `json:"created"`
|
|
VerificationMethod string `json:"verificationMethod"`
|
|
ProofPurpose string `json:"proofPurpose"`
|
|
ProofValue []byte `json:"proofValue"`
|
|
ZKProof []byte `json:"zkProof,omitempty"` // Zero-knowledge proof
|
|
}
|
|
|
|
// Issuer represents a trusted credential issuer
|
|
type Issuer struct {
|
|
ID ids.ID `json:"id"`
|
|
Name string `json:"name"`
|
|
PublicKey []byte `json:"publicKey"`
|
|
Types []string `json:"types"` // Types of credentials they can issue
|
|
TrustLevel int `json:"trustLevel"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// RevocationEntry represents a credential revocation
|
|
type RevocationEntry struct {
|
|
CredentialID ids.ID `json:"credentialId"`
|
|
RevokedBy ids.ID `json:"revokedBy"`
|
|
RevokedAt time.Time `json:"revokedAt"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// VM implements the IdentityVM for decentralized identity
|
|
type VM struct {
|
|
rt *runtime.Runtime
|
|
config Config
|
|
log log.Logger
|
|
db database.Database
|
|
|
|
// Per-VM GPU acceleration session. Reserved for future batch
|
|
// credential signature verification and DID proof checks.
|
|
accel *accel.VMSession
|
|
|
|
// State
|
|
identities map[ids.ID]*Identity
|
|
credentials map[ids.ID]*Credential
|
|
issuers map[ids.ID]*Issuer
|
|
revocations map[ids.ID]*RevocationEntry
|
|
pendingCreds []*Credential
|
|
pendingBlocks map[ids.ID]*Block
|
|
|
|
// work tells consensus a block can be built, which is what issuing a
|
|
// credential produces.
|
|
work vmcore.Latch
|
|
|
|
// Consensus
|
|
lastAccepted *Block
|
|
lastAcceptedID ids.ID
|
|
|
|
// Fee policy gating user-submitted mutating RPCs (CreateIdentity,
|
|
// IssueCredential, RevokeCredential, CreateProof,
|
|
// RegisterIssuer). FlatPolicy at MinTxFeeFloor; consensus-
|
|
// internal callers bypass via direct VM methods.
|
|
feePolicy fee.Policy
|
|
networkID uint32
|
|
|
|
mu sync.RWMutex
|
|
|
|
// RPC
|
|
rpcServer *rpc.Server
|
|
}
|
|
|
|
// Initialize implements chain.ChainVM
|
|
func (vm *VM) Initialize(
|
|
ctx context.Context,
|
|
vmInit vmcore.Init,
|
|
) error {
|
|
vm.rt = vmInit.Runtime
|
|
vm.db = vmInit.DB
|
|
|
|
if logger, ok := vm.rt.Log.(log.Logger); ok {
|
|
vm.log = logger
|
|
} else {
|
|
return errors.New("invalid logger type")
|
|
}
|
|
|
|
vm.identities = make(map[ids.ID]*Identity)
|
|
vm.credentials = make(map[ids.ID]*Credential)
|
|
vm.issuers = make(map[ids.ID]*Issuer)
|
|
vm.revocations = make(map[ids.ID]*RevocationEntry)
|
|
vm.pendingCreds = make([]*Credential, 0)
|
|
vm.pendingBlocks = make(map[ids.ID]*Block)
|
|
|
|
// Parse genesis
|
|
genesis, err := ParseGenesis(vmInit.Genesis)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to parse genesis: %w", err)
|
|
}
|
|
|
|
// Apply configuration
|
|
vm.config = Config{
|
|
CredentialTTL: int64(defaultCredentialTTL.Seconds()),
|
|
MaxClaims: defaultMaxClaims,
|
|
AllowSelfIssue: false,
|
|
RequireZKProofs: false,
|
|
}
|
|
|
|
if genesis.Config != nil {
|
|
if genesis.Config.CredentialTTL > 0 {
|
|
vm.config.CredentialTTL = genesis.Config.CredentialTTL
|
|
}
|
|
if genesis.Config.MaxClaims > 0 {
|
|
vm.config.MaxClaims = genesis.Config.MaxClaims
|
|
}
|
|
vm.config.TrustedIssuers = genesis.Config.TrustedIssuers
|
|
vm.config.AllowSelfIssue = genesis.Config.AllowSelfIssue
|
|
vm.config.RequireZKProofs = genesis.Config.RequireZKProofs
|
|
}
|
|
|
|
// Initialize RPC server
|
|
vm.rpcServer = rpc.NewServer()
|
|
vm.rpcServer.RegisterCodec(grjson.NewCodec(), "application/json")
|
|
vm.rpcServer.RegisterCodec(grjson.NewCodec(), "application/json;charset=UTF-8")
|
|
vm.rpcServer.RegisterService(&Service{vm: vm}, "identity")
|
|
|
|
// Pin fee policy. I-Chain accepts user mutating RPCs so attach
|
|
// the canonical FlatPolicy at MinTxFeeFloor; fee.Validate
|
|
// refuses zero-fee user-facing chains at boot.
|
|
if vm.rt != nil {
|
|
vm.networkID = vm.rt.NetworkID
|
|
}
|
|
vm.feePolicy = newFeePolicy(vm.networkID)
|
|
if err := fee.Validate(vm.feePolicy); err != nil {
|
|
return fmt.Errorf("identityvm: fee policy: %w", err)
|
|
}
|
|
|
|
// Load last accepted block
|
|
if err := vm.loadLastAccepted(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize genesis issuers
|
|
for _, issuer := range genesis.Issuers {
|
|
vm.issuers[issuer.ID] = issuer
|
|
}
|
|
|
|
// Initialize genesis identities
|
|
for _, identity := range genesis.Identities {
|
|
vm.identities[identity.ID] = identity
|
|
}
|
|
|
|
vm.log.Info("IdentityVM initialized",
|
|
log.Int("issuers", len(vm.issuers)),
|
|
log.Int("identities", len(vm.identities)),
|
|
)
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadLastAccepted loads the last accepted block from the database
|
|
func (vm *VM) loadLastAccepted() error {
|
|
lastAcceptedBytes, err := vm.db.Get(lastAcceptedKey)
|
|
if err == database.ErrNotFound {
|
|
vm.lastAccepted = &Block{
|
|
BlockHeight: 0,
|
|
BlockTimestamp: time.Now().Unix(),
|
|
vm: vm,
|
|
status: choices.Accepted,
|
|
}
|
|
vm.lastAcceptedID = vm.lastAccepted.ID()
|
|
return nil
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var blockID ids.ID
|
|
copy(blockID[:], lastAcceptedBytes)
|
|
|
|
blockBytes, err := vm.db.Get(blockID[:])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var block Block
|
|
if err := parseBlock(blockBytes, &block); err != nil {
|
|
return err
|
|
}
|
|
|
|
block.vm = vm
|
|
block.status = choices.Accepted
|
|
vm.lastAccepted = &block
|
|
vm.lastAcceptedID = blockID
|
|
|
|
return nil
|
|
}
|
|
|
|
// SetState implements chain.ChainVM
|
|
func (vm *VM) SetState(ctx context.Context, state uint32) error {
|
|
return nil
|
|
}
|
|
|
|
// NewHTTPHandler implements chain.ChainVM
|
|
func (vm *VM) NewHTTPHandler(ctx context.Context) (http.Handler, error) {
|
|
handlers, err := vm.CreateHandlers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
for path, handler := range handlers {
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
mux.Handle(path, handler)
|
|
}
|
|
return mux, nil
|
|
}
|
|
|
|
// Shutdown implements chain.ChainVM
|
|
func (vm *VM) Shutdown(ctx context.Context) error {
|
|
vm.log.Info("IdentityVM shutting down")
|
|
return nil
|
|
}
|
|
|
|
// CreateHandlers implements chain.ChainVM
|
|
func (vm *VM) CreateHandlers(ctx context.Context) (map[string]http.Handler, error) {
|
|
return map[string]http.Handler{
|
|
"/rpc": vm.rpcServer,
|
|
}, nil
|
|
}
|
|
|
|
// HealthCheck implements chain.ChainVM
|
|
func (vm *VM) HealthCheck(ctx context.Context) (chain.HealthResult, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
return chain.HealthResult{
|
|
Healthy: true,
|
|
Details: map[string]string{
|
|
"identities": fmt.Sprintf("%d", len(vm.identities)),
|
|
"credentials": fmt.Sprintf("%d", len(vm.credentials)),
|
|
"issuers": fmt.Sprintf("%d", len(vm.issuers)),
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Version implements chain.ChainVM
|
|
func (vm *VM) Version(ctx context.Context) (string, error) {
|
|
return "1.0.0", nil
|
|
}
|
|
|
|
// Connected implements chain.ChainVM
|
|
func (vm *VM) Connected(ctx context.Context, nodeID ids.NodeID, nodeVersion *chain.VersionInfo) error {
|
|
vm.log.Debug("Node connected", log.String("nodeID", nodeID.String()))
|
|
return nil
|
|
}
|
|
|
|
// Disconnected implements chain.ChainVM
|
|
func (vm *VM) Disconnected(ctx context.Context, nodeID ids.NodeID) error {
|
|
vm.log.Debug("Node disconnected", log.String("nodeID", nodeID.String()))
|
|
return nil
|
|
}
|
|
|
|
// BuildBlock implements chain.ChainVM
|
|
func (vm *VM) BuildBlock(ctx context.Context) (chain.Block, error) {
|
|
vm.mu.Lock()
|
|
defer vm.mu.Unlock()
|
|
|
|
// A block with nothing in it says nothing and still has to be voted on.
|
|
if len(vm.pendingCreds) == 0 {
|
|
return nil, errNothingToBuild
|
|
}
|
|
|
|
// Copy pending credentials to avoid slice mutation issues during Accept
|
|
creds := make([]*Credential, len(vm.pendingCreds))
|
|
copy(creds, vm.pendingCreds)
|
|
|
|
block := &Block{
|
|
ParentID_: vm.lastAcceptedID,
|
|
BlockHeight: vm.lastAccepted.BlockHeight + 1,
|
|
BlockTimestamp: time.Now().Unix(),
|
|
Credentials: creds,
|
|
vm: vm,
|
|
status: choices.Processing,
|
|
}
|
|
|
|
vm.pendingBlocks[block.ID()] = block
|
|
|
|
return block, nil
|
|
}
|
|
|
|
// ParseBlock implements chain.ChainVM
|
|
func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (chain.Block, error) {
|
|
var block Block
|
|
if err := parseBlock(blockBytes, &block); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block.vm = vm
|
|
block.bytes = blockBytes
|
|
|
|
return &block, nil
|
|
}
|
|
|
|
// GetBlock implements chain.ChainVM
|
|
func (vm *VM) GetBlock(ctx context.Context, blockID ids.ID) (chain.Block, error) {
|
|
vm.mu.RLock()
|
|
// Check pending blocks (nil-safe for early calls before initialization)
|
|
if vm.pendingBlocks != nil {
|
|
if block, ok := vm.pendingBlocks[blockID]; ok {
|
|
vm.mu.RUnlock()
|
|
return block, nil
|
|
}
|
|
}
|
|
vm.mu.RUnlock()
|
|
|
|
blockBytes, err := vm.db.Get(blockID[:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var block Block
|
|
if err := parseBlock(blockBytes, &block); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
block.vm = vm
|
|
block.bytes = blockBytes
|
|
block.status = choices.Accepted
|
|
|
|
return &block, nil
|
|
}
|
|
|
|
// SetPreference implements chain.ChainVM
|
|
func (vm *VM) SetPreference(ctx context.Context, blockID ids.ID) error {
|
|
return nil
|
|
}
|
|
|
|
// LastAccepted implements chain.ChainVM
|
|
func (vm *VM) LastAccepted(ctx context.Context) (ids.ID, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
return vm.lastAcceptedID, nil
|
|
}
|
|
|
|
// ======== Identity Management ========
|
|
|
|
// CreateIdentity creates a new decentralized identity
|
|
func (vm *VM) CreateIdentity(publicKey []byte, metadata map[string]string) (*Identity, error) {
|
|
vm.mu.Lock()
|
|
defer vm.mu.Unlock()
|
|
|
|
// Generate identity ID from public key
|
|
h := sha256.New()
|
|
h.Write(publicKey)
|
|
binary.Write(h, binary.BigEndian, time.Now().UnixNano())
|
|
identityID := ids.ID(h.Sum(nil))
|
|
|
|
// Generate DID
|
|
did := fmt.Sprintf("did:lux:%s", identityID.String()[:16])
|
|
|
|
identity := &Identity{
|
|
ID: identityID,
|
|
DID: did,
|
|
PublicKey: publicKey,
|
|
Created: time.Now(),
|
|
Updated: time.Now(),
|
|
Metadata: metadata,
|
|
Services: make([]ServiceEndpoint, 0),
|
|
}
|
|
|
|
vm.identities[identityID] = identity
|
|
|
|
// Persist (native ZAP wire)
|
|
identityBytes := marshalIdentity(identity)
|
|
key := append(identityPrefix, identityID[:]...)
|
|
if err := vm.db.Put(key, identityBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return identity, nil
|
|
}
|
|
|
|
// GetIdentity returns an identity by ID
|
|
func (vm *VM) GetIdentity(identityID ids.ID) (*Identity, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
identity, ok := vm.identities[identityID]
|
|
if !ok {
|
|
return nil, errUnknownIdentity
|
|
}
|
|
return identity, nil
|
|
}
|
|
|
|
// ResolveIdentity resolves an identity by DID
|
|
func (vm *VM) ResolveIdentity(did string) (*Identity, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
for _, identity := range vm.identities {
|
|
if identity.DID == did {
|
|
return identity, nil
|
|
}
|
|
}
|
|
return nil, errUnknownIdentity
|
|
}
|
|
|
|
// ======== Credential Management ========
|
|
|
|
// IssueCredential issues a new verifiable credential
|
|
func (vm *VM) IssueCredential(issuerID, subjectID ids.ID, credType []string, claims map[string]interface{}, ttl time.Duration) (*Credential, error) {
|
|
vm.mu.Lock()
|
|
defer vm.mu.Unlock()
|
|
|
|
// Verify issuer exists and is authorized
|
|
issuer, ok := vm.issuers[issuerID]
|
|
if !ok && !vm.config.AllowSelfIssue {
|
|
return nil, errNotIssuer
|
|
}
|
|
|
|
if issuer != nil && issuer.Status != "active" {
|
|
return nil, errNotIssuer
|
|
}
|
|
|
|
// Verify subject exists
|
|
if _, ok := vm.identities[subjectID]; !ok {
|
|
return nil, errUnknownIdentity
|
|
}
|
|
|
|
// Verify claims count
|
|
if len(claims) > vm.config.MaxClaims {
|
|
return nil, errors.New("too many claims")
|
|
}
|
|
|
|
// Generate credential ID
|
|
h := sha256.New()
|
|
h.Write(issuerID[:])
|
|
h.Write(subjectID[:])
|
|
binary.Write(h, binary.BigEndian, time.Now().UnixNano())
|
|
credID := ids.ID(h.Sum(nil))
|
|
|
|
// Calculate expiration
|
|
expiration := time.Now().Add(ttl)
|
|
if ttl == 0 {
|
|
expiration = time.Now().Add(time.Duration(vm.config.CredentialTTL) * time.Second)
|
|
}
|
|
|
|
cred := &Credential{
|
|
ID: credID,
|
|
Type: credType,
|
|
Issuer: issuerID,
|
|
Subject: subjectID,
|
|
IssuanceDate: time.Now(),
|
|
ExpirationDate: expiration,
|
|
Claims: claims,
|
|
Status: CredentialActive,
|
|
}
|
|
|
|
vm.credentials[credID] = cred
|
|
vm.pendingCreds = append(vm.pendingCreds, cred)
|
|
|
|
// Consensus builds nothing until it is told there is something to build.
|
|
vm.work.Signal()
|
|
|
|
return cred, nil
|
|
}
|
|
|
|
// GetCredential returns a credential by ID
|
|
func (vm *VM) GetCredential(credID ids.ID) (*Credential, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
cred, ok := vm.credentials[credID]
|
|
if !ok {
|
|
return nil, errUnknownCredential
|
|
}
|
|
|
|
// Check expiration
|
|
if time.Now().After(cred.ExpirationDate) {
|
|
cred.Status = CredentialExpired
|
|
}
|
|
|
|
// Check revocation
|
|
if _, revoked := vm.revocations[credID]; revoked {
|
|
cred.Status = CredentialRevoked
|
|
}
|
|
|
|
return cred, nil
|
|
}
|
|
|
|
// RevokeCredential revokes a credential
|
|
func (vm *VM) RevokeCredential(credID ids.ID, revokerID ids.ID, reason string) error {
|
|
vm.mu.Lock()
|
|
defer vm.mu.Unlock()
|
|
|
|
cred, ok := vm.credentials[credID]
|
|
if !ok {
|
|
return errUnknownCredential
|
|
}
|
|
|
|
// Verify revoker is issuer or subject
|
|
if cred.Issuer != revokerID && cred.Subject != revokerID {
|
|
return errors.New("not authorized to revoke")
|
|
}
|
|
|
|
cred.Status = CredentialRevoked
|
|
|
|
revocation := &RevocationEntry{
|
|
CredentialID: credID,
|
|
RevokedBy: revokerID,
|
|
RevokedAt: time.Now(),
|
|
Reason: reason,
|
|
}
|
|
|
|
vm.revocations[credID] = revocation
|
|
|
|
// Persist revocation (native ZAP wire)
|
|
revBytes := marshalRevocation(revocation)
|
|
key := append(revocationPrefix, credID[:]...)
|
|
return vm.db.Put(key, revBytes)
|
|
}
|
|
|
|
// VerifyCredential verifies a credential is valid
|
|
func (vm *VM) VerifyCredential(credID ids.ID) (bool, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
cred, ok := vm.credentials[credID]
|
|
if !ok {
|
|
return false, errUnknownCredential
|
|
}
|
|
|
|
// Check status
|
|
if cred.Status == CredentialRevoked {
|
|
return false, errCredentialRevoked
|
|
}
|
|
|
|
// Check expiration
|
|
if time.Now().After(cred.ExpirationDate) {
|
|
return false, errCredentialExpired
|
|
}
|
|
|
|
// Check revocation registry
|
|
if _, revoked := vm.revocations[credID]; revoked {
|
|
return false, errCredentialRevoked
|
|
}
|
|
|
|
// When the chain requires ZK proofs, a credential must carry one. Gating the
|
|
// requirement on cred.Proof != nil made the requirement optional: a credential
|
|
// with no proof object at all skipped the check and verified.
|
|
if vm.config.RequireZKProofs {
|
|
if cred.Proof == nil || len(cred.Proof.ZKProof) == 0 {
|
|
return false, errInvalidProof
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// CreateCredentialProof creates a CredentialProof artifact
|
|
func (vm *VM) CreateCredentialProof(credID ids.ID, zkProof []byte, selectiveDisclosure []string) (*artifacts.CredentialProof, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
cred, ok := vm.credentials[credID]
|
|
if !ok {
|
|
return nil, errUnknownCredential
|
|
}
|
|
|
|
// Get issuer and subject DIDs
|
|
issuer, ok := vm.identities[cred.Issuer]
|
|
issuerDID := ""
|
|
if ok {
|
|
issuerDID = issuer.DID
|
|
}
|
|
|
|
subject, ok := vm.identities[cred.Subject]
|
|
subjectDID := ""
|
|
if ok {
|
|
subjectDID = subject.DID
|
|
}
|
|
|
|
// Create claims commitment
|
|
claimsBytes, _ := json.Marshal(cred.Claims)
|
|
claimsCommitment := sha256.Sum256(claimsBytes)
|
|
|
|
// Determine credential type
|
|
credType := ""
|
|
if len(cred.Type) > 0 {
|
|
credType = cred.Type[0]
|
|
}
|
|
|
|
proof := &artifacts.CredentialProof{
|
|
Version_: 1,
|
|
SigSuite_: artifacts.SuitePQOnly,
|
|
CredentialID: credID,
|
|
IssuerDID: issuerDID,
|
|
SubjectDID: subjectDID,
|
|
CredType: credType,
|
|
ClaimsCommitment: claimsCommitment,
|
|
SelectiveProof: zkProof,
|
|
IssuedAt: cred.IssuanceDate,
|
|
ExpiresAt: cred.ExpirationDate,
|
|
RevocationEpoch: cred.RevocationIndex,
|
|
}
|
|
|
|
return proof, nil
|
|
}
|
|
|
|
// GetBlockIDAtHeight returns the block ID at a given height
|
|
func (vm *VM) GetBlockIDAtHeight(ctx context.Context, height uint64) (ids.ID, error) {
|
|
// Height index not implemented - return error
|
|
return ids.Empty, errors.New("height index not implemented")
|
|
}
|
|
|
|
// WaitForEvent implements chain.ChainVM
|
|
// WaitForEvent blocks until there is a credential to build a block from, or the
|
|
// VM stops. Waiting only on the context would mean BuildBlock is never called
|
|
// and the chain never leaves genesis, however many credentials are issued.
|
|
func (vm *VM) WaitForEvent(ctx context.Context) (vmcore.Message, error) {
|
|
return vm.work.WaitForEvent(ctx)
|
|
}
|
|
|
|
// ======== Issuer Management ========
|
|
|
|
// RegisterIssuer registers a new credential issuer
|
|
func (vm *VM) RegisterIssuer(name string, publicKey []byte, types []string, trustLevel int) (*Issuer, error) {
|
|
vm.mu.Lock()
|
|
defer vm.mu.Unlock()
|
|
|
|
// Generate issuer ID
|
|
h := sha256.New()
|
|
h.Write(publicKey)
|
|
issuerID := ids.ID(h.Sum(nil))
|
|
|
|
issuer := &Issuer{
|
|
ID: issuerID,
|
|
Name: name,
|
|
PublicKey: publicKey,
|
|
Types: types,
|
|
TrustLevel: trustLevel,
|
|
CreatedAt: time.Now(),
|
|
Status: "active",
|
|
}
|
|
|
|
vm.issuers[issuerID] = issuer
|
|
|
|
// Persist
|
|
issuerBytes, _ := json.Marshal(issuer)
|
|
key := append(issuerPrefix, issuerID[:]...)
|
|
if err := vm.db.Put(key, issuerBytes); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return issuer, nil
|
|
}
|
|
|
|
// GetIssuer returns an issuer by ID
|
|
func (vm *VM) GetIssuer(issuerID ids.ID) (*Issuer, error) {
|
|
vm.mu.RLock()
|
|
defer vm.mu.RUnlock()
|
|
|
|
issuer, ok := vm.issuers[issuerID]
|
|
if !ok {
|
|
return nil, errors.New("unknown issuer")
|
|
}
|
|
return issuer, nil
|
|
}
|
|
|
|
// ======== Genesis ========
|
|
|
|
// Genesis represents genesis data for IdentityVM
|
|
type Genesis struct {
|
|
Timestamp int64 `json:"timestamp"`
|
|
Config *Config `json:"config,omitempty"`
|
|
Issuers []*Issuer `json:"issuers,omitempty"`
|
|
Identities []*Identity `json:"identities,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
// ParseGenesis parses genesis bytes
|
|
func ParseGenesis(genesisBytes []byte) (*Genesis, error) {
|
|
var genesis Genesis
|
|
if len(genesisBytes) > 0 {
|
|
if err := json.Unmarshal(genesisBytes, &genesis); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if genesis.Timestamp == 0 {
|
|
genesis.Timestamp = time.Now().Unix()
|
|
}
|
|
|
|
return &genesis, nil
|
|
}
|