Umul MVP 기술 설계¶
작성일: 2026-07-26
상태: 승인
대상 네트워크: XMTP dev, GIWA Sepolia
1. 목표¶
Umul MVP는 지갑 주소를 신원으로 사용하는 모바일형 웹 메신저입니다. 이메일, 전화번호, 비밀번호 없이 지갑 연결과 서명으로 로그인합니다. 1:1 및 그룹 메시지는 XMTP dev 네트워크로 전송합니다.
Dojang 인증은 메시징 권한을 제한하지 않습니다. 인증된 주소에는 Verified 배지를 표시하고, 미인증 주소도 대화를 만들고 메시지를 주고받을 수 있습니다. 대화 내용을 GIWA Sepolia에 증빙할 때만 모든 참여자의 Dojang 인증을 요구합니다.
P0 완료 범위는 다음과 같습니다.
- 지갑 로그인과 기본 채팅 UX
- XMTP 기반 1:1 및 그룹 E2EE 메시징
- Dojang 상태 표시와 수신함 분류
- 참여자 전원 서명 기반 대화 온체인 증빙
- 제공된 세 테스트 주소의 실제 XMTP 메시징 검증
2. 제외 범위¶
다음 기능은 P1 이후로 미룹니다.
- CLAIR 기반 리스크 탐지
- 토큰 송금, 구매, 원화 결제
- 가스비 대납과 온오프램프
- 별도 메시징 프로토콜 선택 기능
- 법적 효력에 대한 판단과 자문
- 네이티브 모바일 앱 구현
테스트 ERC-20 주소는 P1 결제 기능에서 사용합니다. P0 코드에는 결제 흐름을 넣지 않습니다.
3. 기술 선택¶
| 영역 | 기술 | 선택 이유 |
|---|---|---|
| 프론트엔드 | React, TypeScript, Vite | 모바일형 웹 UI와 브라우저 XMTP SDK 통합 |
| 메시징 | XMTP Browser SDK, XMTP dev | 브라우저에서 지갑 서명과 MLS E2EE 처리 |
| 지갑·체인 | EIP-1193, wagmi, viem | 지갑 연결, GIWA Sepolia 조회와 서명 |
| 백엔드 | Node.js, TypeScript, Fastify | XMTP Node SDK를 사용할 수 있고 API 구현이 단순함 |
| 데이터베이스 | PostgreSQL | 증빙 요청과 서명 상태의 일관성 관리 |
| 컨트랙트 | Solidity, Foundry | EIP-712 서명 검증과 GIWA Sepolia 배포 |
| 테스트 | Vitest, Testing Library, Playwright, Foundry | 단위, 통합, 실제 사용자 흐름 검증 |
공식 XMTP 채팅 SDK 목록에는 Go SDK가 없습니다. 백엔드는 Node.js와 TypeScript를 사용합니다. 사용자 메시징은 브라우저에서 처리하고, XMTP Node SDK는 E2E 도구나 서버 측 통합이 필요한 경우에만 사용합니다.
4. 저장소 구조¶
언어와 배포 단위를 기준으로 최상위 디렉터리를 분리합니다. 공용 제품 코드를 담는 packages 디렉터리는 두지 않습니다.
umul/
├── frontend/
│ ├── src/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── infrastructure/
│ │ └── presentation/
│ └── tests/
├── backend/
│ ├── src/
│ │ ├── domain/
│ │ ├── application/
│ │ ├── infrastructure/
│ │ └── interfaces/
│ ├── migrations/
│ └── tests/
├── contracts/
│ ├── src/
│ ├── test/
│ ├── script/
│ └── deployments/
├── mobile/
│ └── README.md
├── docs/
│ ├── plans/
│ ├── architecture/
│ ├── api/
│ └── runbooks/
├── compose.yaml
└── README.md
루트에는 통합 실행 설정과 전체 프로젝트 문서만 둡니다.
API 계약은 백엔드가 소유합니다. 생성된 프론트엔드 API 클라이언트는 frontend 내부에 둡니다. 컨트랙트 ABI는 contracts가 원본을 소유하고 프론트엔드와 백엔드는 각자의 infrastructure 디렉터리에 생성된 사본을 둡니다.
5. 컴포넌트 경계¶
5.1 프론트엔드¶
프론트엔드는 메시지 원문을 다루는 유일한 Umul 컴포넌트입니다.
- domain: 주소, 대화, 수신함 분류, Dojang 상태, 증빙 manifest 규칙
- application: 로그인, 대화 동기화, 전송, 분류, 증빙 생성과 서명 유스케이스
- infrastructure: 지갑, XMTP Browser SDK, Dojang RPC, 백엔드 API, 컨트랙트 연결
- presentation: 화면, 컴포넌트, 화면 상태와 스타일
presentation은 SDK를 직접 호출하지 않습니다. application 계층의 인터페이스만 사용합니다. XMTP, Dojang, API, 컨트랙트 구현체는 애플리케이션 시작 시 주입합니다.
5.2 백엔드¶
백엔드는 일반 메시지의 원문과 XMTP 복호화 키를 저장하지 않습니다.
- 지갑 로그인 nonce와 세션 관리
- 증빙 요청 식별자와 상태 관리
- 참여자별 서명 수집
- 트랜잭션 해시와 확인 상태 관리
- API 계약과 입력 검증
5.3 컨트랙트¶
증빙 컨트랙트는 다음 작업만 수행합니다.
- 증빙 요청의 EIP-712 서명 검증
- 참여자 주소의 정렬, 중복, 누락 검사
- 모든 참여자의 Dojang 인증 상태 조회
- evidence ID 재사용 방지
- 증빙 해시와 메타데이터 기록
- 증빙 완료 이벤트 발행
컨트랙트는 대화 원문과 개별 메시지 원문을 저장하지 않습니다.
5.4 모바일¶
이번 단계에서는 네이티브 모바일 코드를 만들지 않습니다. mobile/README.md에 프론트엔드 유스케이스와 화면 모델을 모바일로 옮길 때의 경계를 기록합니다.
6. 사용자 흐름¶
6.1 지갑 로그인¶
- 사용자가 GIWA Wallet, MetaMask 또는 WalletConnect를 선택합니다.
- 프론트엔드는 GIWA Sepolia 연결 상태를 확인합니다.
- 백엔드가 nonce를 발급합니다.
- 사용자가 로그인 메시지에 서명합니다.
- 백엔드는 서명을 검증하고 HttpOnly 세션 쿠키를 발급합니다.
- XMTP Browser SDK가 Inbox를 생성하거나 복구합니다.
- 앱은 채팅 목록으로 이동합니다.
지갑 연결이나 서명을 거부하면 온보딩 화면으로 돌아갑니다. 세션이 유효하고 XMTP 로컬 데이터베이스가 있으면 재실행 시 채팅 목록으로 바로 이동합니다.
6.2 대화 목록과 Dojang 분류¶
프론트엔드는 XMTP 대화를 동기화한 뒤 참여자의 지갑 주소를 확인합니다. 주소별 DojangScroll.isVerified() 결과는 메모리에 한 시간 보관합니다. 앱을 다시 실행하면 캐시를 초기화합니다.
1:1 대화는 상대방의 상태를 사용합니다.
- Verified 상대방: 일반함
- Unverified 상대방: 요청함
- Dojang RPC 오류: 확인 불가 상태
- 사용자가 스팸으로 지정: 스팸함
그룹은 자신을 제외한 참여자 전원이 Verified이면 일반함에 둡니다. 한 명이라도 Unverified이면 요청함에 둡니다. 그룹 헤더에는 인증 N/M을 표시합니다.
요청함은 읽기 전용 격리 공간이 아닙니다. 사용자는 Unverified 상대방과 대화를 시작하고 메시지를 보내며 답장할 수 있습니다.
Dojang RPC 오류를 Unverified로 바꾸지 않습니다. 확인 불가 상태에서도 메시징은 허용하고 증빙만 차단합니다.
6.3 새 대화¶
- 사용자가 지갑 주소를 입력합니다.
- 프론트엔드는 주소 형식과 XMTP 수신 가능 여부를 확인합니다.
- Dojang 상태를 조회해 배지를 표시합니다.
- Verified 여부와 관계없이 사용자가 대화를 생성할 수 있습니다.
- 그룹은 복수 주소를 입력하고 XMTP MLS 그룹으로 생성합니다.
6.4 메시지 송수신¶
- 1:1은 XMTP DM을 사용합니다.
- 그룹은 XMTP MLS 그룹을 사용합니다.
- 앱은 모든 대화의 메시지 스트림을 구독합니다.
- 전송 실패 메시지는 화면에 남기고 재시도를 제공합니다.
- 스트림이 중단되면 연결 상태를 표시하고 제한된 지수 백오프로 재연결합니다.
- XMTP 초기화 오류가 나도 로컬 데이터베이스를 자동 삭제하지 않습니다.
7. 온체인 증빙¶
7.1 증빙 manifest¶
프론트엔드는 선택한 메시지로 정규화된 manifest를 만듭니다.
- 포맷 버전
- XMTP conversation ID의 해시
- 오름차순으로 정렬된 참여자 주소
- 메시지 ID
- 발신자 Inbox ID
- 전송 시각
- 각 메시지 원문의 해시
- 전체 content hash
- 재사용 방지 nonce
메시지는 전송 시각과 메시지 ID 순으로 정렬합니다. 같은 대화와 메시지 집합은 같은 content hash를 만들어야 합니다.
7.2 제안과 서명¶
- 요청자가 메시지 범위를 선택합니다.
- 프론트엔드가 manifest와 content hash를 만듭니다.
- 모든 참여자의 Dojang 상태를 캐시 없이 다시 조회합니다.
- 미검증 또는 확인 불가 참여자가 있으면 증빙을 시작하지 않습니다.
- 증빙 제안을 XMTP 암호화 제어 메시지로 참여자에게 전송합니다.
- 참여자는 자신의 로컬 메시지로 content hash를 다시 계산합니다.
- 일치하면 동일한 EIP-712 데이터에 지갑 서명합니다.
- 백엔드는 content hash, 참여자, 서명 상태와 서명값을 저장합니다.
- 참여자 전원의 서명이 모이면 요청 상태를 READY로 바꿉니다.
백엔드는 선택한 메시지의 원문을 받지 않습니다.
7.3 트랜잭션 제출¶
READY 상태의 참여자 중 한 명이 GIWA Sepolia 트랜잭션을 제출합니다. 컨트랙트는 다음 순서로 검사합니다.
- evidence ID가 사용되지 않았는지 확인합니다.
- 참여자 목록이 정렬됐고 중복이 없는지 확인합니다.
- 각 참여자의 EIP-712 서명을 검증합니다.
- 각 참여자에 대해
DojangScroll.isVerified()를 호출합니다. - 증빙 해시와 메타데이터를 기록합니다.
- 참여자와 증빙 정보를 포함한 이벤트를 발행합니다.
프론트엔드 사전 검사와 컨트랙트 검사를 함께 사용합니다. 인증이 서명 후 철회되거나 만료된 경우 컨트랙트가 제출을 거부합니다.
7.4 온체인 데이터¶
컨트랙트가 저장하거나 이벤트로 발행할 값은 다음과 같습니다.
- evidence ID
- content hash
- conversation ID 해시
- participants hash
- 참여자 주소
- 메시지 수
- 대화 시작·종료 시각
- 기록 시각
- 제출자
S6 증빙 상세 화면은 체인 데이터를 원본으로 사용합니다. 백엔드 데이터는 진행 상태와 조회 편의를 위한 파생 데이터입니다.
XMTP 그룹 참여자 집합을 컨트랙트가 독립적으로 확인할 방법은 없습니다. 참여자 전원이 같은 participants hash에 서명하는 방식으로 선언된 집합에 동의합니다. 이 한계와 법적 효력 미확정 상태를 사용자 문서에 적습니다.
8. 백엔드 상태와 데이터¶
증빙 요청 상태는 다음과 같습니다.
PROPOSED -> SIGNING -> READY -> SUBMITTED -> CONFIRMED
| | | |
+------> REJECTED +------> FAILED
+------> EXPIRED
상태 전이는 트랜잭션 안에서 검사합니다. API 재시도는 idempotency key로 중복 요청과 중복 서명을 막습니다.
PostgreSQL에는 다음 범주의 데이터만 저장합니다.
- 지갑 세션과 nonce
- 증빙 요청과 참여자
- content hash와 manifest 버전
- 참여자 서명과 서명 시각
- 트랜잭션 해시, 블록 번호, 확인 상태
- 생성, 만료, 거부 시각
일반 메시지 원문, XMTP 개인키, 지갑 개인키는 저장하지 않습니다.
9. 오류 처리¶
| 오류 | 사용자 처리 | 시스템 처리 |
|---|---|---|
| 지갑 연결·서명 거부 | 온보딩 복귀, 재시도 | 세션 생성 안 함 |
| 체인 불일치 | 네트워크 전환 안내 | 트랜잭션 차단 |
| XMTP 초기화 실패 | 원인과 재시도 표시 | 로컬 DB 보존 |
| XMTP 스트림 중단 | 연결 상태 표시 | 제한된 백오프 재연결 |
| Dojang RPC 실패 | 확인 불가 표시 | 메시징 허용, 증빙 차단 |
| 메시지 전송 실패 | 실패 버블과 재시도 | 중복 전송 방지 |
| 증빙 해시 불일치 | 서명 거부 | 대화 재동기화 요구 |
| 참여자 서명 거부 | 거부 상태 표시 | 요청을 REJECTED로 전이 |
| Dojang 인증 철회 | 제출 실패 사유 표시 | 컨트랙트에서 최종 차단 |
| 트랜잭션 실패 | 재제출 제공 | 수집한 서명 보존 |
| 중복 증빙 | 이미 기록됨 표시 | evidence ID로 거부 |
10. 테스트 전략¶
새 동작은 테스트를 먼저 작성하고 실패를 확인한 뒤 구현합니다.
10.1 프론트엔드¶
- 주소 정규화와 축약
- 1:1 및 그룹 Dojang 분류
- Dojang 확인 불가 처리
- 증빙 manifest 정규화와 해시
- 화면 상태와 사용자 입력
- SDK 포트의 성공, 실패, 재시도
10.2 백엔드¶
- 로그인 nonce 일회성
- 세션 소유자 확인
- 증빙 상태 전이
- 중복 요청과 중복 서명 방지
- 참여자가 아닌 주소의 접근 차단
- 실제 PostgreSQL을 사용한 repository 통합 테스트
10.3 컨트랙트¶
- 참여자 전원 서명 성공
- 누락, 중복, 잘못된 서명 거부
- 미검증 참여자 거부
- evidence ID 재사용 거부
- 정렬되지 않은 참여자 거부
- 퍼즈 테스트와 상태 불변성 검사
- GIWA Sepolia fork 또는 실제 배포본 통합 검사
10.4 E2E¶
Playwright는 세 개의 독립 사용자 세션을 실행합니다. 테스트 개인키는 실행 환경에서만 주입하고 소스, fixture, 스냅숏과 로그에 남기지 않습니다.
XMTP 호출을 대체한 테스트는 실제 메시징 완료 증거로 인정하지 않습니다.
11. 완료 조건¶
11.1 세 테스트 주소의 실제 메시징¶
다음 주소를 A, B, C로 사용합니다.
- A:
0xEf792c4973Cc8ef6aF433B3bd9f4461B701301f6 - B:
0x532dA989489eaB825d484EAd09f117018da47778 - C:
0x2F9713FB44eE7A15EF393eA694959324Be7a6479
완료 조건은 다음과 같습니다.
- A, B, C가 지갑 서명으로 로그인합니다.
- 세 주소가 각각 XMTP dev Inbox를 생성하거나 복구합니다.
- A와 B가 1:1 메시지를 왕복합니다.
- B와 C가 1:1 메시지를 왕복합니다.
- A가 A, B, C를 포함하는 그룹을 만듭니다.
- 세 사용자가 그룹에 각각 메시지를 전송합니다.
- 나머지 두 사용자가 각 메시지를 실제 XMTP 스트림으로 수신합니다.
- 새로고침 후 대화와 메시지가 다시 동기화됩니다.
- 인증 발급 전 A는 TESTNET_FAUCET 기준 Verified, B와 C는 Unverified로 표시됩니다.
- 이 혼합 상태에서 세 사용자 모두 메시지를 계속 보낼 수 있습니다.
- 증빙 성공 검증을 위해 B가 인증된 뒤에도 C는 Unverified 상태로 메시징에 참여합니다.
검증 기록에는 개인키를 제외한 XMTP 메시지 ID, 송신 주소, 수신 주소와 시각을 남깁니다.
11.2 전체 P0¶
- S1부터 S7까지 모바일 웹 흐름이 동작합니다.
- 세 테스트 주소의 실제 1:1 및 그룹 XMTP 메시징이 성공합니다.
- Dojang 조회와 Verified, Unverified, 확인 불가 상태가 동작합니다.
- 일반함, 요청함, 스팸함 분류가 동작합니다.
- 미검증 사용자가 메시지를 주고받을 수 있습니다.
- 미검증 참여자가 포함된 증빙을 차단합니다.
- 참여자 전원이 검증된 대화의 EIP-712 다중 서명과 GIWA Sepolia 증빙이 성공합니다.
- 증빙 상세에서 GIWA Explorer 링크를 열 수 있습니다.
- 프론트엔드, 백엔드, 컨트랙트 테스트와 빌드가 통과합니다.
- 개인키와 로컬 환경변수가 Git 이력에 없습니다.
- 실행법, API, DB, 컨트랙트와 검증 결과가 문서화돼 있습니다.
12. 테스트넷 의존성¶
2026-07-26 실측에서 A만 TESTNET_FAUCET Verified이고 B와 C는 미검증입니다. UPBIT_KOREA 기준으로는 세 주소 모두 미검증입니다.
현재 상태로 다음 흐름을 실제 네트워크에서 검증할 수 있습니다.
- Verified 및 Unverified 배지
- 일반함과 요청함 분류
- 인증과 관계없는 1:1 및 그룹 메시징
- 미검증 참여자를 포함한 증빙 차단
GIWA Sepolia에서 다중 서명 증빙 성공까지 검증하려면 같은 대화에 참여하는 주소 전원이 프로젝트가 설정한 Dojang attester 기준으로 Verified여야 합니다. A, B, C의 혼합 상태 메시징과 라우팅을 먼저 기록한 뒤 B만 테스트넷 인증을 받습니다. A와 B의 1:1 대화로 증빙 성공을 확인하고, C가 포함된 3인 그룹에서는 증빙 차단과 메시징 허용을 함께 확인합니다. 구현 중 테스트넷 인증 절차를 runbook으로 작성하고 완료 보고 전에 실제 성공 트랜잭션을 확인합니다.
13. 보안 기준¶
- 제공된 테스트 개인키를 Git에 기록하지 않습니다.
- 개인키를 프론트엔드 번들, API 응답과 로그에 넣지 않습니다.
- 백엔드는 사용자의 메시지 원문과 XMTP 복호화 키를 받지 않습니다.
- 세션 쿠키에는 HttpOnly, Secure, SameSite 설정을 적용합니다.
- 주소와 서명 입력은 서버와 컨트랙트에서 다시 검증합니다.
- 증빙 해시는 버전이 포함된 정규화 규칙으로 계산합니다.
- 컨트랙트 배포 주소와 chain ID를 서명 도메인에 포함해 재사용을 막습니다.
- 오류 로그는 서명 원문과 환경변수를 마스킹합니다.
14. 단계별 구현 순서¶
각 단계는 구현, 테스트, 실제 흐름 검증, 커밋을 마친 뒤 다음 단계로 이동합니다.
- 저장소 기반과 개발 환경
- 지갑 로그인과 모바일 셸
- XMTP 1:1 메시징
- XMTP 그룹 메시징
- Dojang 상태와 수신함 분류
- 증빙 컨트랙트
- 증빙 백엔드와 프론트엔드 플로우
- 세 사용자 실제 E2E와 문서
P1 기능은 P0 검증이 끝나기 전에 구현하지 않습니다.