프로젝트

일반

사용자정보

Actions

이슈 #312

진행중
CP JL

17-3. 노드 추가 API (지갑 연동)

이슈 #312: 17-3. 노드 추가 API (지갑 연동)

Chris Park이(가) 약 7시간 전에 추가함.

상태:
해결
우선순위:
높음
담당자:
목표버전:
시작일:
2026/05/19
완료일:
2026/05/21 (약 5달 지연)
진척도:

0%

추정시간:

설명

POST /api/nodes


연결된 일감 1 (한 건 진행 중 — 모두 미완료)

다음 일감과 관련됨: 이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)해결James Lee2026/04/16

Actions

CP Chris Park이(가) 7달 전에 변경 Actions #1

  • 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
  • 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.

JL James Lee이(가) 7달 전에 변경 Actions #2

17-3. 노드 추가 API 구현 완료

NodeService.NodeChallenge + NodeService.RegisterNode RPC 를 구현했습니다. 지갑 소유자가 자신의 지갑으로 직접 엣지/가상 노드를 추가할 수 있는 사용자 플로우를 제공합니다.

  • Repository: alphea-ai-edge
  • Commit: commit:server|920cd5c — feat: implement NodeService NodeChallenge and RegisterNode
  • 관련 이슈: #254 (DB 쓰기 경로 추가)

인증 플로우

Client                                  Server
  │                                       │
  │  NodeChallenge(node_id, wallet) ───→  │
  │                                       │  ChallengeStore.issue("node:<id>")
  │  ←─── challenge + expires_at ─────   │
  │                                       │
  │  (wallet 개인키로 challenge 서명)      │
  │                                       │
  │  RegisterNode(node_id, challenge,     │
  │    signature, wallet, type, tier,     │
  │    device_meta) ───────────────→     │
  │                                       │  1. ChallengeStore.verify (일회용)
  │                                       │  2. ECDSA recover → address 비교
  │                                       │  3. 소유권 체크 (기존 행 있으면)
  │                                       │  4. upsert_registered
  │                                       │  5. Node JWT 발급
  │  ←─── access_token + NodeInfo ─────  │

Proto 정의

rpc NodeChallenge(NodeChallengeRequest) returns (NodeChallengeResponse);
rpc RegisterNode(RegisterNodeRequest) returns (RegisterNodeResponse);

message NodeChallengeRequest {
  string node_id = 1;
  string owner_wallet_address = 2;
}

message RegisterNodeRequest {
  string node_id = 1;
  string challenge = 2;
  string signature = 3;
  string owner_wallet_address = 4;
  NodeType node_type = 5;
  NodeTier tier = 6;
  DeviceMeta device_meta = 7;
}

message RegisterNodeResponse {
  string access_token = 1;
  string token_type = 2;
  int32 expires_in = 3;
  NodeInfo node = 4;
}

핵심 구현 포인트

1. Challenge 네임스페이싱

하나의 in-process ChallengeStore 를 AuthHandler 와 NodeHandler 가 공유합니다. 키 충돌을 막기 위해 NodeChallenge 는 "node:" + node_id 형식으로 저장합니다. 동일 지갑이 동시에 wallet 로그인 (AuthService/Challenge) 과 노드 등록을 진행해도 서로 간섭하지 않습니다.

2. 실제 ECDSA recovery 기반 소유권 증명

Step C 에서 구현한 domain::auth::verify_signature 를 그대로 사용합니다. 요청의 owner_wallet_address 로 서명을 검증하므로, 악의적인 사용자가 타인의 지갑 주소로 노드를 등록하는 것은 암호학적으로 불가능합니다.

3. 소유권 체크 (upsert 전)

if let Some(existing) = get_by_id(pool, &node_id).await? {
    if !existing.owner_wallet_address.eq_ignore_ascii_case(&req.owner_wallet_address) {
        return Err(Status::permission_denied("node_id is registered to a different wallet"));
    }
}

이미 등록된 node_id 를 다른 지갑이 탈취하는 것을 2중으로 방지합니다 (서명 검증이 1차, 소유권 체크가 2차).

4. 멱등 upsert

INSERT INTO nodes (node_id, name, node_type, owner_wallet_address, auth_mode, status, tier, role, device_meta, device_platform)
VALUES ($1, $2, $3, $4, 2, $5, $6, 1, $7, $8)
ON CONFLICT (node_id) DO UPDATE SET
    device_meta = EXCLUDED.device_meta,
    device_platform = EXCLUDED.device_platform,
    status = $5,
    updated_at = NOW()
RETURNING ...
  • auth_mode = 2 (SIGNATURE), status = 2 (ACTIVE), role = 1 (WORKER) 고정
  • node_type / tier 는 첫 등록 시에만 기록되고 재등록으로 변경되지 않음 (ON CONFLICT DO UPDATE 절이 건드리지 않음)
  • 재등록은 device_meta / device_platform / status 만 갱신 → 모바일/엣지 노드가 재접속 시 다시 ACTIVE 로 복귀하는 경로

5. Node JWT 발급

generate_access_token(
    &req.owner_wallet_address,
    "node",
    Some(NODE_SCOPES.iter().map(|s| s.to_string()).collect()),
    &self.state.cfg.jwt_secret,
)

Token 내용:

  • typ = "node"
  • scope = ["node:heartbeat", "jobs:claim", "jobs:report"] (APP_SCOPES 는 포함하지 않음 — 레거시와 동일)
  • exp = iat + 3600

레거시 대비 업그레이드

레거시 동작 (Go): RegisterNode 호출 시 이미 존재하는 노드만 처리하고 없으면 NotFound 를 반환했습니다. 즉, 관리자가 웹 UI 로 선등록한 노드에 대해서만 authenticate 가 가능했습니다.

이번 포팅: 사용자 지갑으로 새 노드 추가 + 기존 노드 재등록 을 모두 지원합니다. Redmine #312 의 "노드 추가 API" 제목 요구사항을 충족하기 위한 업그레이드입니다. Step C 에서 실제 ECDSA recovery 가 구현되어 악용이 불가능하므로 안전합니다.

E2E 검증 결과

examples/node_register.rs 실행 로그 (일부):

→ owner wallet: 0x217d4acd078cad11ab7ca93da7a4368b1393383d
→ node_id:      edge-9a778fea
→ challenge:    ALPHEA Edge Network... Nonce: 4ea9a24b7f60a80d02fc3cdd505c8995
→ signature:    0x897cd899ec0acc...1b

=== RegisterNode succeeded ===
token_type:   Bearer
expires_in:   3600
node:         id=edge-9a778fea type=1 tier=2 status=2 auth_mode=2

=== GetNode round-trip ===
node:         id=edge-9a778fea owner=0x217d4acd078cad11ab7ca93da7a4368b1393383d device_platform=edge

=== Re-registration (upsert) succeeded ===

DB 확인:

node_id node_type tier status auth_mode owner_wallet_address device_platform
edge-9a778fea 1 (EDGE) 2 (MID) 2 (ACTIVE) 2 (SIGNATURE) 0x217d4acd... edge

에러 경로 검증 (grpcurl)

케이스 응답
잘못된 challenge PermissionDenied: invalid or expired challenge
빈 필수 인자 InvalidArgument: node_id, challenge, signature, and owner_wallet_address are required
NodeType UNSPECIFIED InvalidArgument: node_type is required

알려진 한계 (후속 단계)

  • 인증 없이 호출 가능: 현재 NodeChallenge / RegisterNode 는 전부 public RPC 로 구성되어 있음 (레거시 allow-list 와 동일). 이 RPC 들 자체가 "처음 인증하는 경로" 이므로 설계상 public 이어야 함
  • 노드 삭제: #313 (Section 17 Step 3) 에서 JWT 인터셉터 와 함께 구현 예정
  • Heartbeat / ConnectNode: 여전히 stub. bidi streaming + JWT 인터셉터 와 함께 별도 스프린트 예정
  • 이름 자동 생성: proto RegisterNodeRequest 에 name 필드가 없어 "Node {short}" 형식으로 derive. 클라이언트 지정 이름이 필요하면 proto 확장 필요

JL James Lee이(가) 7달 전에 변경 Actions #3

  • 상태을(를) 신규에서 해결(으)로 변경되었습니다.

17-3 작업 완료 — 해결 전환

  • Section 17 Step 2 에서 NodeService.NodeChallenge + RegisterNode RPC 구현 완료 (commit:server|920cd5c)
  • 실제 ECDSA recovery 기반 소유권 증명 + 신규 생성/재등록 멱등 upsert
  • Section 17 Step 3 에서 재등록 = un-archive 동작 추가 (commit:server|d9668e6) — 삭제됐던 노드를 동일 node_id 로 재등록하면 자동 복원

E2E 검증 완료.

JL James Lee이(가) 7달 전에 변경 Actions #4

  • 다음 일감과 관련됨:에 이슈 #379: NodeService.Heartbeat 구현 (alphea-ai-edge)이(가) 추가되었습니다.
Actions

내보내기 PDF Atom