mirror of
https://github.com/luxfi/chains.git
synced 2026-09-24 00:26:00 +00:00
Accept set the status and moved lastAccepted before writing anything, then issued one Put per credential with the error DROPPED on the credential writes themselves. A failure partway left earlier credentials on disk, the tip already advanced, and the chain believing a block it had only half applied. It now goes through the shared store: the credentials are staged, committed in one batch with the block and the tip, and the revocations, identities and status become visible only afterwards. A failure discards all of it and leaves the tip where it was. The dropped error is checked. Beside the fix, what this chain kept restating goes away: the pending queue becomes the shared pool, the blocks in flight and the tip come from the store under its one lock, the fee declaration is a value rather than a policy plus a network id, and the factory no longer half-populates a VM that Initialize rebuilds anyway. Two things change that were not behaviour anyone could rely on. Genesis was stamped with time.Now, so its id — the hash of its own fields — differed on every node and every restart; it now comes from genesis itself, as on K and F. And the per-VM GPU session the factory allocated was never read and never released. Restoring the original Accept fails all three atomicity tests.
757 lines
21 KiB
Go
757 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"
|
|
"time"
|
|
|
|
"github.com/gorilla/rpc/v2"
|
|
grjson "github.com/gorilla/rpc/v2/json"
|
|
|
|
"github.com/luxfi/chains/chain"
|
|
"github.com/luxfi/consensus/core/choices"
|
|
"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"
|
|
vmchain "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 (
|
|
_ vmchain.ChainVM = (*VM)(nil)
|
|
|
|
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"`
|
|
}
|
|
|
|
// MaxPendingCredentials bounds the queue of credentials waiting for a block.
|
|
// Issuing is open to any authorized issuer, so without a bound the queue is
|
|
// whatever they choose to make it.
|
|
const MaxPendingCredentials = 4096
|
|
|
|
// VM implements the IdentityVM for decentralized identity
|
|
type VM struct {
|
|
rt *runtime.Runtime
|
|
config Config
|
|
log log.Logger
|
|
|
|
// chain is the durable state, the blocks in flight and the tip — and the
|
|
// one lock over all of it, which the record caches below share. Take it
|
|
// with chain.Lock or chain.RLock.
|
|
chain *chain.Store
|
|
|
|
// Record caches, under the chain's lock.
|
|
identities map[ids.ID]*Identity
|
|
credentials map[ids.ID]*Credential
|
|
issuers map[ids.ID]*Issuer
|
|
revocations map[ids.ID]*RevocationEntry
|
|
|
|
// pending holds the credentials waiting for a block, and tells consensus
|
|
// there is one to build. LOCK ORDER: the chain's lock, then the pool's.
|
|
pending *chain.Pool[*Credential]
|
|
|
|
// fee is what this chain charges to admit a mutating RPC. I-Chain accepts
|
|
// user-submitted DID and credential calls that produce on-chain effects,
|
|
// so it declares the floor rather than admitting free work.
|
|
fee chain.Fee
|
|
|
|
// RPC
|
|
rpcServer *rpc.Server
|
|
}
|
|
|
|
// Initialize implements chain.ChainVM
|
|
func (vm *VM) Initialize(
|
|
ctx context.Context,
|
|
vmInit vmcore.Init,
|
|
) error {
|
|
vm.rt = vmInit.Runtime
|
|
|
|
if logger, ok := vm.rt.Log.(log.Logger); ok {
|
|
vm.log = logger
|
|
} else {
|
|
return errors.New("invalid logger type")
|
|
}
|
|
|
|
// The caches are rebuilt only here: a block publishes its records after its
|
|
// writes have committed, so a block that fails leaves them untouched and
|
|
// there is nothing to roll back.
|
|
vm.chain = chain.New(vmInit.DB, nil)
|
|
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.pending = chain.NewPool(MaxPendingCredentials, func(c *Credential) ids.ID { return c.ID })
|
|
|
|
// 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")
|
|
|
|
// I-Chain accepts user mutating RPCs, so it declares the floor; the node's
|
|
// boot-time Validate refuses a zero-fee user-facing chain.
|
|
var networkID uint32
|
|
if vm.rt != nil {
|
|
networkID = vm.rt.NetworkID
|
|
}
|
|
vm.fee = chain.Floor(networkID)
|
|
if err := fee.Validate(vm.fee.Policy()); err != nil {
|
|
return fmt.Errorf("identityvm: fee policy: %w", err)
|
|
}
|
|
|
|
// Genesis is at height 0, stamped with the time genesis itself declares.
|
|
// Reading the wall clock here would give every node a different genesis id
|
|
// for the same chain, since the id is the hash of the block's own fields.
|
|
genesisBlock := &Block{
|
|
BlockTimestamp: genesis.Timestamp,
|
|
vm: vm,
|
|
status: choices.Accepted,
|
|
}
|
|
if _, _, err := vm.chain.Open(genesisBlock, vm.parseBlock); 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
|
|
}
|
|
|
|
// parseBlock decodes a block belonging to this VM. The store uses it to read
|
|
// back the tip and any accepted block, so there is one decoder rather than one
|
|
// per call site.
|
|
func (vm *VM) parseBlock(raw []byte) (chain.Block, error) {
|
|
var block Block
|
|
if err := parseBlock(raw, &block); err != nil {
|
|
return nil, err
|
|
}
|
|
block.vm = vm
|
|
block.bytes = raw
|
|
block.status = choices.Accepted
|
|
return &block, nil
|
|
}
|
|
|
|
// credentialKey is where a credential is recorded.
|
|
func credentialKey(id ids.ID) []byte {
|
|
return append(append([]byte(nil), credentialPrefix...), id[:]...)
|
|
}
|
|
|
|
// 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) (vmchain.HealthResult, error) {
|
|
vm.chain.RLock()
|
|
defer vm.chain.RUnlock()
|
|
|
|
return vmchain.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 *vmchain.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. Reading the tip and registering the
|
|
// block on it happen in one step, so nothing can be accepted in between and
|
|
// leave the proposal hanging off a parent that is no longer the tip.
|
|
func (vm *VM) BuildBlock(ctx context.Context) (vmchain.Block, error) {
|
|
built, err := vm.chain.Propose(func(parent ids.ID, height uint64) (chain.Block, error) {
|
|
// A block with nothing in it says nothing and still has to be voted on.
|
|
creds := vm.pending.Take(0)
|
|
if len(creds) == 0 {
|
|
return nil, errNothingToBuild
|
|
}
|
|
return &Block{
|
|
ParentID_: parent,
|
|
BlockHeight: height + 1,
|
|
BlockTimestamp: time.Now().Unix(),
|
|
Credentials: creds,
|
|
vm: vm,
|
|
status: choices.Processing,
|
|
}, nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return built, nil
|
|
}
|
|
|
|
// ParseBlock implements chain.ChainVM
|
|
func (vm *VM) ParseBlock(ctx context.Context, blockBytes []byte) (vmchain.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) (vmchain.Block, error) {
|
|
return vm.chain.Block(blockID, vm.parseBlock)
|
|
}
|
|
|
|
// 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) {
|
|
id, _ := vm.chain.Tip()
|
|
return id, nil
|
|
}
|
|
|
|
// ======== Identity Management ========
|
|
|
|
// CreateIdentity creates a new decentralized identity
|
|
func (vm *VM) CreateIdentity(publicKey []byte, metadata map[string]string) (*Identity, error) {
|
|
vm.chain.Lock()
|
|
defer vm.chain.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.chain.Base().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.chain.RLock()
|
|
defer vm.chain.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.chain.RLock()
|
|
defer vm.chain.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.chain.Lock()
|
|
defer vm.chain.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
|
|
|
|
// Queueing tells consensus there is a block to build; a chain builds
|
|
// nothing until it is told.
|
|
if err := vm.pending.Add(cred); err != nil {
|
|
delete(vm.credentials, credID)
|
|
return nil, err
|
|
}
|
|
|
|
return cred, nil
|
|
}
|
|
|
|
// GetCredential returns a credential by ID
|
|
func (vm *VM) GetCredential(credID ids.ID) (*Credential, error) {
|
|
vm.chain.RLock()
|
|
defer vm.chain.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.chain.Lock()
|
|
defer vm.chain.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.chain.Base().Put(key, revBytes)
|
|
}
|
|
|
|
// VerifyCredential verifies a credential is valid
|
|
func (vm *VM) VerifyCredential(credID ids.ID) (bool, error) {
|
|
vm.chain.RLock()
|
|
defer vm.chain.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.chain.RLock()
|
|
defer vm.chain.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.pending.Wait(ctx)
|
|
}
|
|
|
|
// ======== Issuer Management ========
|
|
|
|
// RegisterIssuer registers a new credential issuer
|
|
func (vm *VM) RegisterIssuer(name string, publicKey []byte, types []string, trustLevel int) (*Issuer, error) {
|
|
vm.chain.Lock()
|
|
defer vm.chain.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.chain.Base().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.chain.RLock()
|
|
defer vm.chain.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
|
|
}
|