이슈 #312
진행중17-3. 노드 추가 API (지갑 연동)
설명
POST /api/nodes
CP Chris Park이(가) 7달 전에 변경
- 프로젝트을(를) 14에서 Alphea App(으)로 변경되었습니다.
- 목표버전을(를) 6월 중간 버전(으)로 지정되었습니다.
JL James Lee이(가) 7달 전에 변경
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달 전에 변경
- 상태을(를) 신규에서 해결(으)로 변경되었습니다.
17-3 작업 완료 — 해결 전환¶
- Section 17 Step 2 에서
NodeService.NodeChallenge+RegisterNodeRPC 구현 완료 (commit:server|920cd5c) - 실제 ECDSA recovery 기반 소유권 증명 + 신규 생성/재등록 멱등 upsert
- Section 17 Step 3 에서 재등록 = un-archive 동작 추가 (commit:server|d9668e6) — 삭제됐던 노드를 동일
node_id로 재등록하면 자동 복원
E2E 검증 완료.