package node import ( "context" "encoding/json" "errors" "fmt" "oc-discovery/conf" "oc-discovery/daemons/node/common" "oc-discovery/daemons/node/indexer" "oc-discovery/daemons/node/pubsub" "oc-discovery/daemons/node/stream" "sync" "time" oclib "cloud.o-forge.io/core/oc-lib" "cloud.o-forge.io/core/oc-lib/dbs" "cloud.o-forge.io/core/oc-lib/models/peer" "cloud.o-forge.io/core/oc-lib/tools" "github.com/google/uuid" "github.com/libp2p/go-libp2p" pubsubs "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/network" pp "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/protocol" "github.com/libp2p/go-libp2p/p2p/security/noise" ) type Node struct { *common.LongLivedStreamRecordedService[interface{}] // change type of stream PS *pubsubs.PubSub IndexerService *indexer.IndexerService PubSubService *pubsub.PubSubService StreamService *stream.StreamService PeerID pp.ID isIndexer bool peerRecord *indexer.PeerRecord // peerSearches: one active peer search per user; new search cancels previous. peerSearches *common.SearchTracker Mu sync.RWMutex } func InitNode(isNode bool, isIndexer bool) (*Node, error) { if !isNode && !isIndexer { return nil, errors.New("wait... what ? your node need to at least something. Retry we can't be friend in that case") } logger := oclib.GetLogger() logger.Info().Msg("retrieving private key...") priv, err := tools.LoadKeyFromFilePrivate() // your node private key if err != nil { return nil, err } logger.Info().Msg("retrieving psk file...") psk, err := common.LoadPSKFromFile() // network common private Network. Public OC PSK is Public Network if err != nil { return nil, nil } logger.Info().Msg("open a host...") gater := newOCConnectionGater(nil) // host set below after creation h, err := libp2p.New( libp2p.PrivateNetwork(psk), libp2p.Identity(priv), libp2p.Security(noise.ID, noise.New), libp2p.ListenAddrStrings( fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", conf.GetConfig().NodeEndpointPort), ), libp2p.ConnectionGater(gater), ) gater.host = h // wire host back into gater now that it exists logger.Info().Msg("Host open on " + h.ID().String()) if err != nil { return nil, errors.New("no host no node") } node := &Node{ PeerID: h.ID(), isIndexer: isIndexer, LongLivedStreamRecordedService: common.NewStreamRecordedService[interface{}](h, 1000), peerSearches: common.NewSearchTracker(), } // Register the bandwidth probe handler so any peer measuring this node's // throughput can open a dedicated probe stream and read the echo. h.SetStreamHandler(common.ProtocolBandwidthProbe, common.HandleBandwidthProbe) // Register the witness query handler so peers can ask this node's view of indexers. h.SetStreamHandler(common.ProtocolWitnessQuery, func(s network.Stream) { common.HandleWitnessQuery(h, s) }) var ps *pubsubs.PubSub if isNode { logger.Info().Msg("generate opencloud node...") ps, err = pubsubs.NewGossipSub(context.Background(), node.Host) if err != nil { panic(err) // can't run your node without a propalgation pubsub, of state of node. } node.PS = ps // buildRecord returns a fresh signed PeerRecord as JSON, embedded in each // heartbeat so the receiving indexer can republish it to the DHT directly. // peerRecord is nil until claimInfo runs, so the first ~20s heartbeats carry // no record — that's fine, claimInfo publishes once synchronously at startup. buildRecord := func() json.RawMessage { if node.peerRecord == nil { return nil } priv, err := tools.LoadKeyFromFilePrivate() if err != nil { return nil } fresh := *node.peerRecord fresh.PeerRecordPayload.ExpiryDate = time.Now().UTC().Add(2 * time.Minute) payload, _ := json.Marshal(fresh.PeerRecordPayload) fresh.Signature, err = priv.Sign(payload) if err != nil { return nil } b, _ := json.Marshal(fresh) return json.RawMessage(b) } logger.Info().Msg("connect to indexers...") common.ConnectToIndexers(node.Host, conf.GetConfig().MinIndexer, conf.GetConfig().MaxIndexer, buildRecord) logger.Info().Msg("claims my node...") if _, err := node.claimInfo(conf.GetConfig().Name, conf.GetConfig().Hostname); err != nil { panic(err) } logger.Info().Msg("run garbage collector...") node.StartGC(30 * time.Second) if node.StreamService, err = stream.InitStream(context.Background(), node.Host, node.PeerID, 1000, node); err != nil { panic(err) } node.StreamService.IsPeerKnown = func(pid pp.ID) bool { // 1. Local DB: known peer (handles blacklist). access := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil) results := access.Search(&dbs.Filters{ And: map[string][]dbs.Filter{ "peer_id": {{Operator: dbs.EQUAL.String(), Value: pid.String()}}, }, }, pid.String(), false) for _, item := range results.Data { p, ok := item.(*peer.Peer) if !ok || p.PeerID != pid.String() { continue } return p.Relation != peer.BLACKLIST } // 2. Ask a connected indexer → DHT lookup by peer_id. for _, addr := range common.Indexers.GetAddrs() { ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second) s, err := h.NewStream(ctx, addr.Info.ID, common.ProtocolGet) cancel() if err != nil { continue } json.NewEncoder(s).Encode(indexer.GetValue{PeerID: pid.String()}) var resp indexer.GetResponse json.NewDecoder(s).Decode(&resp) s.Reset() return resp.Found } return false } if node.PubSubService, err = pubsub.InitPubSub(context.Background(), node.Host, node.PS, node, node.StreamService); err != nil { panic(err) } f := func(ctx context.Context, evt common.Event, topic string) { m := map[string]interface{}{} err := json.Unmarshal(evt.Payload, &m) if err != nil || evt.From == node.PeerID.String() { return } if p, err := node.GetPeerRecord(ctx, evt.From); err == nil && len(p) > 0 && m["search"] != nil { node.StreamService.SendResponse(p[0], &evt, fmt.Sprintf("%v", m["search"])) } } logger.Info().Msg("subscribe to decentralized search flow...") go node.SubscribeToSearch(node.PS, &f) logger.Info().Msg("connect to NATS") go ListenNATS(node) logger.Info().Msg("Node is actually running.") } if isIndexer { logger.Info().Msg("generate opencloud indexer...") node.IndexerService = indexer.NewIndexerService(node.Host, ps, 500) } return node, nil } func (d *Node) Close() { if d.isIndexer && d.IndexerService != nil { d.IndexerService.Close() } d.PubSubService.Close() d.StreamService.Close() d.Host.Close() } func (d *Node) publishPeerRecord( rec *indexer.PeerRecord, ) error { priv, err := tools.LoadKeyFromFilePrivate() // your node private key if err != nil { return err } for _, ad := range common.Indexers.GetAddrs() { var err error if common.Indexers.Streams, err = common.TempStream(d.Host, *ad.Info, common.ProtocolPublish, "", common.Indexers.Streams, map[protocol.ID]*common.ProtocolInfo{}, &common.Indexers.MuStream); err != nil { continue } stream := common.Indexers.Streams.GetPerID(common.ProtocolPublish, ad.Info.ID) base := indexer.PeerRecordPayload{ Name: rec.Name, DID: rec.DID, PubKey: rec.PubKey, ExpiryDate: time.Now().UTC().Add(2 * time.Minute), } payload, _ := json.Marshal(base) rec.PeerRecordPayload = base rec.Signature, err = priv.Sign(payload) if err := json.NewEncoder(stream.Stream).Encode(&rec); err != nil { // then publish on stream return err } } return nil } // SearchPeerRecord starts a distributed peer search via ProtocolSearchPeer. // A new call for the same userKey cancels any previous search. // Results are pushed to onResult as they arrive; the function returns when // the stream closes (idle timeout, explicit cancel, or indexer unreachable). func (d *Node) SearchPeerRecord(userKey, needle string, onResult func(common.SearchHit)) { logger := oclib.GetLogger() idleTimeout := common.SearchIdleTimeout() ctx, cancel := context.WithCancel(context.Background()) // Register cancels any previous search for userKey and starts the idle timer. // The composite key doubles as QueryID so the indexer echoes it back. searchKey := d.peerSearches.Register(userKey, cancel, idleTimeout) defer d.peerSearches.Cancel(searchKey) req := common.SearchPeerRequest{QueryID: searchKey} if pid, err := pp.Decode(needle); err == nil { req.PeerID = pid.String() } else if _, err := uuid.Parse(needle); err == nil { req.DID = needle } else { req.Name = needle } for _, ad := range common.Indexers.GetAddrs() { if ad.Info == nil { continue } dialCtx, dialCancel := context.WithTimeout(ctx, 5*time.Second) s, err := d.Host.NewStream(dialCtx, ad.Info.ID, common.ProtocolSearchPeer) dialCancel() if err != nil { continue } if err := json.NewEncoder(s).Encode(req); err != nil { s.Reset() continue } // Interrupt the blocking Decode as soon as the context is cancelled // (idle timer, explicit PB_CLOSE_SEARCH, or replacement search). go func() { <-ctx.Done() s.SetReadDeadline(time.Now()) }() seen := map[string]struct{}{} dec := json.NewDecoder(s) for { var result common.SearchPeerResult if err := dec.Decode(&result); err != nil { break } if result.QueryID != searchKey || !d.peerSearches.IsActive(searchKey) { break } d.peerSearches.ResetIdle(searchKey) for _, hit := range result.Records { key := hit.PeerID if key == "" { key = hit.DID } if _, already := seen[key]; already { continue } seen[key] = struct{}{} onResult(hit) } } s.Reset() return } logger.Warn().Str("user", userKey).Msg("[search] no reachable indexer for peer search") } func (d *Node) GetPeerRecord( ctx context.Context, pidOrdid string, ) ([]*peer.Peer, error) { var err error var info map[string]indexer.PeerRecord // Build the GetValue request: if pidOrdid is neither a UUID DID nor a libp2p // PeerID, treat it as a human-readable name and let the indexer resolve it. // GetPeerRecord resolves by PeerID or DID only. // Name-based search goes through SearchPeerRecord (ProtocolSearchPeer). getReq := indexer.GetValue{Key: pidOrdid} if pidR, pidErr := pp.Decode(pidOrdid); pidErr == nil { getReq.PeerID = pidR.String() getReq.Key = "" } for _, ad := range common.Indexers.GetAddrs() { if common.Indexers.Streams, err = common.TempStream(d.Host, *ad.Info, common.ProtocolGet, "", common.Indexers.Streams, map[protocol.ID]*common.ProtocolInfo{}, &common.Indexers.MuStream); err != nil { continue } stream := common.Indexers.Streams.GetPerID(common.ProtocolGet, ad.Info.ID) if err := json.NewEncoder(stream.Stream).Encode(getReq); err != nil { continue } var resp indexer.GetResponse if err := json.NewDecoder(stream.Stream).Decode(&resp); err != nil { continue } if resp.Found { info = resp.Records } break } var ps []*peer.Peer for _, pr := range info { if pk, err := pr.Verify(); err != nil { return nil, err } else if _, p, err := pr.ExtractPeer(d.PeerID.String(), pr.PeerID, pk); err != nil { return nil, err } else { ps = append(ps, p) } } return ps, err } func (d *Node) claimInfo( name string, endPoint string, // TODO : endpoint is not necesserry StreamAddress ) (*peer.Peer, error) { if endPoint == "" { return nil, errors.New("no endpoint found for peer") } did := uuid.New().String() peers := oclib.NewRequestAdmin(oclib.LibDataEnum(oclib.PEER), nil).Search(&dbs.Filters{ And: map[string][]dbs.Filter{ // search by name if no filters are provided "peer_id": {{Operator: dbs.EQUAL.String(), Value: d.Host.ID().String()}}, }, }, "", false) if len(peers.Data) > 0 { did = peers.Data[0].GetID() // if already existing set up did as made } priv, err := tools.LoadKeyFromFilePrivate() if err != nil { return nil, err } pub, err := tools.LoadKeyFromFilePublic() if err != nil { return nil, err } pubBytes, err := crypto.MarshalPublicKey(pub) if err != nil { return nil, err } now := time.Now().UTC() expiry := now.Add(150 * time.Second) pRec := indexer.PeerRecordPayload{ Name: name, DID: did, // REAL PEER ID PubKey: pubBytes, ExpiryDate: expiry, } d.PeerID = d.Host.ID() payload, _ := json.Marshal(pRec) rec := &indexer.PeerRecord{ PeerRecordPayload: pRec, } rec.Signature, err = priv.Sign(payload) if err != nil { return nil, err } rec.PeerID = d.Host.ID().String() rec.APIUrl = endPoint rec.StreamAddress = "/ip4/" + conf.GetConfig().Hostname + "/tcp/" + fmt.Sprintf("%v", conf.GetConfig().NodeEndpointPort) + "/p2p/" + rec.PeerID rec.NATSAddress = oclib.GetConfig().NATSUrl rec.WalletAddress = "my-wallet" if err := d.publishPeerRecord(rec); err != nil { return nil, err } d.peerRecord = rec if _, err := rec.Verify(); err != nil { return nil, err } else { _, p, err := rec.ExtractPeer(did, did, pub) b, err := json.Marshal(p) if err != nil { return p, err } go tools.NewNATSCaller().SetNATSPub(tools.CREATE_RESOURCE, tools.NATSResponse{ FromApp: "oc-discovery", Datatype: tools.PEER, Method: int(tools.CREATE_RESOURCE), SearchAttr: "peer_id", Payload: b, }) return p, err } } /* TODO: - Le booking est un flow neuf décentralisé : On check on attend une réponse, on valide, il passe par discovery, on relais. - Le shared workspace est une affaire de décentralisation, on communique avec les shared les mouvements - Un shared remplace la notion de partnership à l'échelle de partnershipping -> quand on share un workspace on devient partenaire temporaire qu'on le soit originellement ou non. -> on a alors les mêmes privilèges. - Les orchestrations admiralty ont le même fonctionnement. Un evenement provoque alors une création de clé de service. On doit pouvoir crud avec verification de signature un DBobject. */