~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
488 lines · 16.6 KB Raw History
package requester

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"strings"

	commonv1 "github.com/johnandersen777/socialweb-computer-kcp/api/common/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/market"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/irohdial"
)

type GuestEvent struct {
	Kind string

	Event market.ResolvedRecord

	Payload market.ResolvedRecord

	Address string

	Tag string

	NetworkKind string

	GuestKind string

	Key string

	Ciphertext string

	SecretsVia string

	SecretsCount *int64

	RequesterDID string

	R2Released *bool

	R2Message string

	HostKeys []v1alpha1.ComputeVMHostKey

	Termination *v1alpha1.ContractTermination

	Sandbox *v1alpha1.ContractSandbox

	ObservedAt string
}

func ValidateTicket(address string) (string, error) {
	ticket := strings.TrimSpace(address)
	ticket = strings.TrimPrefix(ticket, "iroh://")
	if ticket == "" || strings.Contains(ticket, "://") || strings.ContainsAny(ticket, " \t\r\n/") {
		return "", fmt.Errorf("requester: %q is not an iroh ticket", address)
	}
	if err := irohdial.CheckTicket(ticket); err != nil {
		return "", fmt.Errorf("requester: %q is not an iroh ticket: %w", address, err)
	}
	return ticket, nil
}

func isGuestEventNSID(nsid string) bool {
	switch nsid {
	case market.ComputeEventsVMOnNetworkNSID, market.ComputeEventsVMHostKeysNSID, market.ComputeEventsVMTerminatedNSID,
		market.ComputeEventsVMSandboxNSID, market.ComputeEventsGuestSecretsRequestedNSID, market.ComputeEventsGuestSecretsAppliedNSID,
		market.ComputeEventsGuestRequesterDIDNSID, market.ComputeEventsGuestR2ReleasedNSID:
		return true
	}
	return isContractEventNSID(nsid)
}

func isContractEventNSID(nsid string) bool {
	switch nsid {
	case market.ComputeEventsGuestPublicKeyNSID, market.ComputeEventsGuestSignalWebRTCNSID:
		return true
	}
	return false
}

func (q *Requester) badgeBlueBinding() func(ctx context.Context, did, key string) (bool, error) {
	return func(ctx context.Context, did, key string) (bool, error) {
		return market.KeyBoundByBadgeBlueKey(ctx, q.repo, did, key), nil
	}
}

func (q *Requester) verifyOptions() market.VerifyRecordOptions {
	return market.VerifyRecordOptions{
		KeysForDID:             market.NewDirectoryKeyLister(q.deps.Directory),
		KeyResolver:            market.DefaultKeyResolver,
		KeyBoundByBadgeBlueKey: q.badgeBlueBinding(),
	}
}

func (q *Requester) receiptOptions() market.VerifyRecordOptions {
	return market.VerifyRecordOptions{
		KeysForDID:  market.NewDirectoryKeyLister(q.deps.Directory),
		KeyResolver: market.DefaultKeyResolver,
	}
}

func (q *Requester) parseGuestEvent(ctx context.Context, event MarketEvent, receiptURI string) (*GuestEvent, error) {
	if event.Event.URI == "" || event.Payload.URI == "" {
		return nil, errors.New("requester: the market event has no event or payload record")
	}
	envelope, err := market.ParseEvent(event.Event.Value)
	if err != nil {
		return nil, err
	}
	payloadNSID := market.NSIDFromURI(event.Payload.URI)
	if !isGuestEventNSID(payloadNSID) {
		return nil, fmt.Errorf("requester: payload %s is not a guest event", payloadNSID)
	}
	ref, err := market.BindEventPayload(*envelope, event.Event.URI, receiptURI, payloadNSID)
	if err != nil {
		return nil, err
	}
	if market.ATURIAuthority(event.Event.URI) != market.ATURIAuthority(receiptURI) {
		return nil, fmt.Errorf("requester: event author is not the bidder")
	}
	opts := q.verifyOptions()
	issuer := market.ATURIAuthority(event.Event.URI)
	if !market.VerifyRecordSignatures(ctx, event.Event.Value, issuer, opts) {
		return nil, fmt.Errorf("requester: event %s has no valid signature", event.Event.URI)
	}
	if !market.VerifyRecordSignatures(ctx, event.Payload.Value, issuer, opts) {
		return nil, fmt.Errorf("requester: payload %s has no valid signature", event.Payload.URI)
	}
	if payloadType := stringOf(event.Payload.Value["$type"]); payloadType != "" && payloadType != payloadNSID {
		return nil, fmt.Errorf("requester: payload %s names type %s", event.Payload.URI, payloadType)
	}
	out := &GuestEvent{Kind: payloadNSID, Event: event.Event, Payload: event.Payload}
	switch payloadNSID {
	case market.ComputeEventsVMOnNetworkNSID:
		address := stringOf(event.Payload.Value["address"])
		tag := stringOf(event.Payload.Value["tag"])
		kind := market.OnNetworkKind(market.ComputeEventsVMOnNetwork{Kind: stringOf(event.Payload.Value["kind"]), Tag: tag})
		switch {
		case kind == market.OnNetworkKindIroh && tag == "":
			if _, err := ValidateTicket(address); err != nil {
				return nil, err
			}
			out.ObservedAt = stringOf(event.Payload.Value["observedAt"])
		case kind == market.OnNetworkKindIroh:
			return nil, fmt.Errorf("requester: the iroh address of %s names tag %q", event.Payload.URI, tag)
		case kind == market.OnNetworkKindSandbox && tag == "":
			if !strings.HasPrefix(address, "sandbox://") {
				return nil, fmt.Errorf("requester: payload %s names the sandbox address %q", event.Payload.URI, address)
			}
			out.ObservedAt = stringOf(event.Payload.Value["createdAt"])
		case tag == "":
			return nil, fmt.Errorf("requester: the %s address of %s names no tag", kind, event.Payload.URI)
		default:
			if err := ValidateNetworkAddress(kind, tag, address); err != nil {
				return nil, err
			}
			out.Tag = tag
			out.ObservedAt = stringOf(event.Payload.Value["createdAt"])
		}
		out.NetworkKind = kind
		out.Address = address
	case market.ComputeEventsGuestPublicKeyNSID, market.ComputeEventsGuestSignalWebRTCNSID:
		guest, err := parseGuestPayload(payloadNSID, event.Payload.Value)
		if err != nil {
			return nil, fmt.Errorf("requester: payload %s: %w", event.Payload.URI, err)
		}
		out.Tag, out.GuestKind, out.Key, out.Ciphertext = guest.Tag, guest.Kind, guest.Key, guest.Ciphertext
		out.ObservedAt = guest.CreatedAt
	case market.ComputeEventsGuestSecretsRequestedNSID:
		record, err := market.ParseComputeEventsGuestSecretsRequested(event.Payload.Value)
		if err != nil {
			return nil, fmt.Errorf("requester: payload %s: %w", event.Payload.URI, err)
		}
		out.ObservedAt = record.CreatedAt
	case market.ComputeEventsGuestR2ReleasedNSID:
		record, err := market.ParseComputeEventsGuestR2Released(event.Payload.Value)
		if err != nil {
			return nil, fmt.Errorf("requester: payload %s: %w", event.Payload.URI, err)
		}
		ok := record.Ok
		out.R2Released, out.R2Message, out.ObservedAt = &ok, record.Message, record.CreatedAt
	case market.ComputeEventsGuestRequesterDIDNSID:
		record, err := market.ParseComputeEventsGuestRequesterDID(event.Payload.Value)
		if err != nil {
			return nil, fmt.Errorf("requester: payload %s: %w", event.Payload.URI, err)
		}
		if err := market.ValidGuestRequesterDID(record.DID, q.repoDID); err != nil {
			return nil, fmt.Errorf("requester: payload %s: %w", event.Payload.URI, err)
		}
		out.RequesterDID = record.DID
		out.ObservedAt = record.CreatedAt
	case market.ComputeEventsGuestSecretsAppliedNSID:
		record, err := market.ParseComputeEventsGuestSecretsApplied(event.Payload.Value)
		if err != nil {
			return nil, fmt.Errorf("requester: payload %s: %w", event.Payload.URI, err)
		}
		switch record.Via {
		case market.GuestSecretsViaGetSecrets, market.GuestSecretsViaSealed:
		default:
			return nil, fmt.Errorf("requester: payload %s names via %q", event.Payload.URI, record.Via)
		}
		out.SecretsVia = record.Via
		if _, ok := event.Payload.Value["count"]; ok && record.Count >= 0 {
			count := record.Count
			out.SecretsCount = &count
		}
		out.ObservedAt = record.CreatedAt
	case market.ComputeEventsVMHostKeysNSID:
		keys, err := q.reportedHostKeys(event.Payload.Value)
		if err != nil {
			return nil, err
		}
		out.HostKeys = keys
		out.ObservedAt = stringOf(event.Payload.Value["observedAt"])
	case market.ComputeEventsVMSandboxNSID:
		sandbox := &v1alpha1.ContractSandbox{ExitCode: int(numberValue(event.Payload.Value["exitCode"])),
			TimedOut: event.Payload.Value["timedOut"] == true, Stdout: stringOf(event.Payload.Value["stdout"]),
			Stderr: stringOf(event.Payload.Value["stderr"]), ObservedAt: stringOf(event.Payload.Value["createdAt"])}
		out.Sandbox = sandbox
		out.ObservedAt = sandbox.ObservedAt
	case market.ComputeEventsVMTerminatedNSID:
		termination, err := reportedTermination(event)
		if err != nil {
			return nil, err
		}
		out.Termination = termination
		out.ObservedAt = termination.ObservedAt
	}
	if out.ObservedAt != "" {
		if _, err := market.ParseTime(out.ObservedAt); err != nil {
			return nil, fmt.Errorf("requester: observedAt: %w", err)
		}
	}
	out.Payload = market.ResolvedRecord{URI: ref.URI, CID: ref.CID, Value: event.Payload.Value}
	return out, nil
}

func (q *Requester) reportedHostKeys(value map[string]any) ([]v1alpha1.ComputeVMHostKey, error) {
	list, ok := value["keys"].([]any)
	if !ok || len(list) == 0 {
		return nil, errors.New("requester: the host-keys event carries no keys")
	}
	raw := make([]v1alpha1.ComputeVMHostKey, 0, len(list))
	for _, item := range list {
		entry, ok := item.(map[string]any)
		if !ok {
			return nil, errors.New("requester: a host key is not an object")
		}
		raw = append(raw, v1alpha1.ComputeVMHostKey{
			Type:        stringOf(entry["type"]),
			PublicKey:   stringOf(entry["publicKey"]),
			Fingerprint: stringOf(entry["fingerprint"]),
		})
	}
	return q.deps.Guests.CheckHostKeys(raw)
}

func reportedTermination(event MarketEvent) (*v1alpha1.ContractTermination, error) {
	reason := stringOf(event.Payload.Value["reason"])
	observedAt := stringOf(event.Payload.Value["observedAt"])
	if reason == "" || observedAt == "" {
		return nil, errors.New("requester: the termination event names no reason or time")
	}
	if _, err := market.ParseTime(observedAt); err != nil {
		return nil, fmt.Errorf("requester: termination time: %w", err)
	}
	termination := &v1alpha1.ContractTermination{
		Reason:     reason,
		ObservedAt: observedAt,
		Event:      commonv1.StrongRef{URI: event.Event.URI, CID: event.Event.CID},
		Payload:    commonv1.StrongRef{URI: event.Payload.URI, CID: event.Payload.CID},
	}
	if code, ok := event.Payload.Value["exitCode"]; ok {
		value := int(numberValue(code))
		termination.ExitCode = &value
	}
	if request, ok := event.Payload.Value["request"].(map[string]any); ok {
		uri := stringOf(request["uri"])
		cid := stringOf(request["cid"])
		if uri != "" {
			termination.Request = &commonv1.StrongRef{URI: uri, CID: cid}
		}
	}
	return termination, nil
}

func sameRecordRef(a, b commonv1.StrongRef) bool {
	if a.URI == "" || b.URI == "" || a.URI != b.URI {
		return false
	}
	return a.CID == "" || b.CID == "" || a.CID == b.CID
}

func sameTermination(a, b *v1alpha1.ContractTermination) bool {
	if a == nil || b == nil {
		return false
	}
	return sameRecordRef(a.Event, b.Event) || sameRecordRef(a.Payload, b.Payload)
}

func terminationRecorded(state *v1alpha1.NodeClaimContract, termination *v1alpha1.ContractTermination) bool {
	if state == nil {
		return false
	}
	return sameTermination(state.Termination, termination)
}

func applyTermination(state *v1alpha1.NodeClaimContract, termination *v1alpha1.ContractTermination, now string) bool {
	if state == nil || termination == nil || terminationRecorded(state, termination) {
		return false
	}
	state.Termination = termination
	state.Outcome = v1alpha1.ContractOutcomeReturned
	state.Phase = v1alpha1.ContractReleased
	if state.Timestamps == nil {
		state.Timestamps = &v1alpha1.ContractTimestamps{}
	}
	state.Timestamps.Released = cmp.Or(state.Timestamps.Released, now)
	return true
}

func terminationMessage(termination *v1alpha1.ContractTermination) string {
	if termination == nil {
		return "the bidder ended the contract"
	}
	return "the bidder ended the contract: " + termination.Reason
}

func rememberGuestEvent(seen map[string]bool, uri string) {
	if uri != "" {
		seen[uri] = true
	}
}

func seedGuestEventSeen(seen map[string]bool, state *v1alpha1.NodeClaimContract) {
	if state == nil {
		return
	}
	if state.Termination != nil {
		rememberGuestEvent(seen, state.Termination.Event.URI)
		rememberGuestEvent(seen, state.Termination.Payload.URI)
	}
	if state.HostKeys != nil {
		rememberGuestEvent(seen, state.HostKeys.Event.URI)
		rememberGuestEvent(seen, state.HostKeys.Payload.URI)
	}
	for _, n := range state.Networks {
		rememberGuestEvent(seen, n.Event.URI)
	}
	if state.SecretsRequested != nil {
		rememberGuestEvent(seen, state.SecretsRequested.Event.URI)
		rememberGuestEvent(seen, state.SecretsRequested.Payload.URI)
	}
	if state.SecretsApplied != nil {
		rememberGuestEvent(seen, state.SecretsApplied.Event.URI)
		rememberGuestEvent(seen, state.SecretsApplied.Payload.URI)
	}
	if state.GuestRequester != nil {
		rememberGuestEvent(seen, state.GuestRequester.Event.URI)
		rememberGuestEvent(seen, state.GuestRequester.Payload.URI)
	}
}

func guestEventSeen(seen map[string]bool, event MarketEvent) bool {
	keys := []string{event.Event.URI, event.Payload.URI}
	found := false
	for _, key := range keys {
		if key != "" && seen[key] {
			found = true
		}
	}
	for _, key := range keys {
		rememberGuestEvent(seen, key)
	}
	return found
}

func guestEventRecorded(eventRef, payloadRef commonv1.StrongRef, parsed *GuestEvent) bool {
	if parsed == nil {
		return false
	}
	return sameRecordRef(eventRef, commonv1.StrongRef{URI: parsed.Event.URI, CID: parsed.Event.CID}) ||
		sameRecordRef(payloadRef, commonv1.StrongRef{URI: parsed.Payload.URI, CID: parsed.Payload.CID})
}

func guestEventApplied(state *v1alpha1.NodeClaimContract, parsed *GuestEvent) bool {
	if state == nil || parsed == nil {
		return false
	}
	switch parsed.Kind {
	case market.ComputeEventsVMOnNetworkNSID:
		if parsed.Tag != "" {
			return networkRecorded(state, parsed)
		}
		return state.VMAddress != ""
	case market.ComputeEventsVMHostKeysNSID:
		return state.HostKeys != nil && guestEventRecorded(state.HostKeys.Event, state.HostKeys.Payload, parsed)
	case market.ComputeEventsVMTerminatedNSID:
		return terminationRecorded(state, parsed.Termination)
	case market.ComputeEventsGuestSecretsRequestedNSID:
		return state.SecretsRequested != nil && guestEventRecorded(state.SecretsRequested.Event, state.SecretsRequested.Payload, parsed)
	case market.ComputeEventsGuestSecretsAppliedNSID:
		return state.SecretsApplied != nil && guestEventRecorded(state.SecretsApplied.Event, state.SecretsApplied.Payload, parsed)
	case market.ComputeEventsGuestRequesterDIDNSID:
		return state.GuestRequester != nil && guestEventRecorded(state.GuestRequester.Event, state.GuestRequester.Payload, parsed)
	}
	return false
}

func laterTime(a, b string) bool {
	ta, errA := market.ParseTime(a)
	tb, errB := market.ParseTime(b)
	if errA != nil || errB != nil {
		return a > b
	}
	return ta.After(tb)
}

func applySecretsEvent(state *v1alpha1.NodeClaimContract, parsed *GuestEvent) bool {
	event := commonv1.StrongRef{URI: parsed.Event.URI, CID: parsed.Event.CID}
	payload := commonv1.StrongRef{URI: parsed.Payload.URI, CID: parsed.Payload.CID}
	switch parsed.Kind {
	case market.ComputeEventsGuestSecretsRequestedNSID:
		if prev := state.SecretsRequested; prev != nil && (guestEventRecorded(prev.Event, prev.Payload, parsed) || laterTime(prev.RequestedAt, parsed.ObservedAt)) {
			return false
		}
		state.SecretsRequested = &v1alpha1.ContractSecretsRequested{RequestedAt: parsed.ObservedAt, Event: event, Payload: payload}
		return true
	case market.ComputeEventsGuestSecretsAppliedNSID:
		if prev := state.SecretsApplied; prev != nil && (guestEventRecorded(prev.Event, prev.Payload, parsed) || laterTime(prev.AppliedAt, parsed.ObservedAt)) {
			return false
		}
		state.SecretsApplied = &v1alpha1.ContractSecretsApplied{AppliedAt: parsed.ObservedAt, Via: parsed.SecretsVia, Count: parsed.SecretsCount,
			Event: event, Payload: payload}
		return true
	}
	return false
}

func secretsRequestPending(state *v1alpha1.NodeClaimContract) bool {
	if state == nil || state.SecretsRequested == nil {
		return false
	}
	applied := state.SecretsApplied
	return applied == nil || laterTime(state.SecretsRequested.RequestedAt, applied.AppliedAt)
}

func hostKeyLines(keys []v1alpha1.ComputeVMHostKey) []string {
	out := make([]string, 0, len(keys))
	for _, key := range keys {
		out = append(out, key.Type+" "+key.PublicKey)
	}
	return out
}

func hostKeyAlias(receiptURI string) string {
	uri, err := market.ParseATURI(receiptURI)
	if err != nil {
		return "swc-guest"
	}
	clean := func(value string) string {
		var b strings.Builder
		for _, r := range value {
			switch {
			case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '.', r == '-', r == '_':
				b.WriteRune(r)
			default:
				b.WriteRune('-')
			}
			if b.Len() >= 60 {
				break
			}
		}
		return strings.Trim(b.String(), "-")
	}
	alias := "swc-guest-" + clean(uri.Authority().String()) + "-" + clean(uri.RecordKey().String())
	if len(alias) > 128 {
		alias = alias[:128]
	}
	return alias
}

func sandboxAddress(address string) bool {
	return strings.HasPrefix(address, "sandbox://")
}