기와장터는 GIWA 체인(이더리움 L2, OP Stack) 위의 3D 소셜 플랫폼입니다. 지갑이 아바타가 되고, P2P 거래가 노점이 되고, 신원 인증이 상인의 뱃지가 됩니다. 설계 원칙은 하나입니다 — 서버는 공간만 담당하고, 가치는 전부 체인 위에서 움직인다.
1시스템 아키텍처 — 서버리스 풀온체인
게임 서버가 없습니다. 브라우저가 GIWA 체인과 직접 읽고 씁니다. 거래·소유·기록은 컨트랙트가 들고, 실시간 위치만 저장 없는 이벤트(비컨)로 흘립니다. 그래서 두 방문자는 서버 없이 서로를 보고 거래합니다.
※ 자금·키는 어떤 서버도 다루지 않는 비수탁 구조. 선택적 Colyseus 서버는 성능(위치 릴레이·읽기 캐시) 용도로만 붙일 수 있고, 없어도 마을은 돕니다 (스케일 로드맵 참고).
openStall 트랜잭션을 보내고 똑같이 가스를 냅니다.
흥정도, 길드 가입도, 도깨비 타격 쿨다운 30초도 그대로 적용됩니다.
- MCP 서버 (
mcp/) — LLM이 마을을 읽고(무료 RPC), 키를 주면 노점 개설·흥정 제안까지 실제 트랜잭션으로 수행합니다. - LLM 상인 NPC (
scripts/merchant-bot.mjs) — 자기 지갑을 든 상인이 들어온 흥정을 페르소나에 따라 판단합니다. 값은 LLM이 제안하고 하한선은 코드가 강제하므로, 모델이 무슨 대답을 하든 헐값 체결은 불가능합니다.
PROTOCOL.md). 붙는 길은 둘입니다 — 체인 경로(정본: 노점·거래·길드·
프레즌스가 전부 온체인)와 룸 경로(선택: 15Hz 위치 동기화).
- 마을 데이터를 기계가 읽는다 — 한옥이 어디 서 있고 무엇이 길을 막는지는
world.json하나에 있습니다. 표를 옮긴 것이 아니라 구운 것이라(npm run export-world) 원본은 여전히 TypeScript 한 곳이고, 굽는 것을 잊으면npm test가 실패합니다. - 룸이 붙이기 쉬운 이유 — 이 서버는 Colyseus의 스키마 상태 동기화를 쓰지
않습니다(직렬화기가
none). 다른 언어로 포팅할 때 제일 어렵고 제일 잘 깨지는 부분인 델타 디코더가 통째로 필요 없다는 뜻입니다 — WebSocket과 msgpack 리더면 됩니다.
npm test는 world.json만
읽는 낯선 클라이언트인 척 마을 전체를 걸어 본 뒤 웹 클라이언트가 밀려난 자리와
1mm 안에서 같은지 대조하고, 실시간 룸 둘(village_live · expedition)이 주고받는 메시지와 정원 · 주기 · 유예 시간이 문서에 다 있는지 대조합니다.
npm run smoke:protocol은 한 걸음 더 가서 문서만 보고 짠 클라이언트로
실제 룸에 입장합니다(colyseus.js를 일부러 쓰지 않습니다 — 쓰면 아무것도 증명하지
못합니다). 이 검증을 처음 돌렸을 때 실제로 문서의 누락이 하나 잡혔습니다:
입장 확인 한 바이트를 돌려보내지 않으면 서버가 모든 메시지를 큐에 쌓아 둡니다 —
소켓은 멀쩡히 열려 있고 스냅샷만 영영 오지 않는, 조용해서 찾기 어려운 종류입니다.
2온체인 컴포넌트 (10종 · v2 묶음 검증 제출 대기)
| 컨트랙트 | 역할 | 주소 |
|---|---|---|
| GiwaMarketV3 | 노점 레지스트리(개설+가격 단일 tx)·에스크로(분쟁/환불)·ERC-1155 쿠폰(선물·소각) | 0x1f3450…36e2 |
| GiwaGuilds | 길드·백층 던전 — 주차 blockhash 시드, settleRun 재계산 검증 |
0xea796f…a31b |
| GiwaPresence | 저장 없는 위치+속도 비컨 이벤트 → 클라 데드레커닝 | 0x4d6006…7a93 |
| GiwaHonors | 소울바운드 칭호 5종 — 다른 컨트랙트 상태로 자격 검증·장착 | 0xeaa867…a8e2 |
| GiwaOffers | 흥정 에스크로 — 수락 시 MarketV3 조합해 buy→confirm→쿠폰 전달 한 tx | 0x534a29…b909 |
| GiwaBoxes | 복주머니(무료·블록해시 확정)·소울바운드 장신구 8종 | 0xeb0349…6b2e |
| GiwaHearth | 모닥불 온기 — 10분 창에 2명↑ 모이면 claim, 장날 2배 | 0xf78026…3365 |
| GiwaWorkshop | 문양 공방(UGC) — 8×8 픽셀 온체인 등록·판매(창작자 직송)·착용 | 0x664762…c975 |
| GiwaBoss | 주간 도깨비 토벌 — 함께 때리는 동시성 코업(블록해시 데미지·온기 보정·장날 2배), 개인·길드 기여, 소울바운드 전리품 | 0x11dbe7…9d63 |
| GiwaProfile | 소셜 프로필 애그리게이터 — profileOf 1콜로 길드·칭호·장신구·문양·온기·전리품 (외부 dApp/SDK 진입점) |
0xf4c2a7…fdf3 |
각 컨트랙트는 클라이언트 src/chain/<모듈>.ts와 1:1로 대응합니다.
카탈로그: contracts/README.md.
외부 발행 DojangScroll(신원 인증)·UP.ID(이름)는 읽기만 연동합니다.
3핵심 플로우 — 그림으로
(a) 노점 구매 + 에스크로 — 가격은 체인이 강제하고, 대금은 확정까지 컨트랙트가 보관합니다.
(b) 던전 — 로컬 즉시 플레이 + settleRun 재계산 검증
문을 열 때마다 tx를 보내지 않고 귀환 한 번으로 전 과정을 검증합니다 — 서버판보다 빠르고, 서버 없이도 트러스트리스. (결정론 결과의 한계는 AI Self-Audit에 공개)
(c) 프레즌스 — 2초 비컨 + 데드레커닝
위치는 분쟁 가치가 없어 체인 상태로 두지 않습니다 — 저장 없는 이벤트라 비컨 1회 ≈ 2µETH. 악의적 좌표는 수신 시 월드 반경으로 클램프, 잔액 부족 시 자동 관전 모드(읽기는 무료).
4재미 설계 — 왜 다시 오는가
지갑이 기록이라면 마을은 장소여야 합니다. 장소가 이기려면 세 가지가 필요합니다: 머무를 이유·만날 시간·자랑할 것. 네 가지 시스템이 이 축을 온체인으로 채웁니다.
손맛은 체인을 기다리지 않습니다. 도깨비를 때린 그 프레임에 섬광·화면 흔들림·효과음이
나가고, 얼마나 깎였는지는 영수증이 확정한 뒤 숫자로 떠오릅니다 — 남이 때린 것도 함께.
즉각 반응은 화면이, 사실 확인은 체인이 합니다. 연출은 새 메시도 파티클도 만들지 않고
이미 있는 카메라·재질·라이트 값을 몇 프레임 흔드는 것이라 저사양에서도 비용이 거의 없고,
prefers-reduced-motion이면 흔들림만 끕니다. 효과음도 파일 0개 —
배경음과 같은 평조 5음 어휘를 씁니다(성공은 올라가고 실패는 내려갑니다).
노점은 주인이 접속을 끊어도 마을에 남습니다(레지스트리·가격 모두 온체인) — 콜드 스타트를 유저 생성 콘텐츠가 공간을 채워 풉니다. 참고 결: 마비노기 모닥불(함께 있는 것 자체가 콘텐츠), 리니지 혈맹(소유의 정치), 로블록스/마크(플랫폼은 도구·유저는 콘텐츠).
5기술 스택 & 품질
컨트랙트: Solidity 0.8.36 · 10종 (ERC-1155 자체 구현 포함, 외부 의존성 0)
서버: 없음 (선택적 Colyseus는 성능 캐시용) · 배포: GitHub Actions → Pages
코드:
src/chain/<모듈>.ts가 컨트랙트와 1:1 대응
npm run smoke:realtime이 실제 소켓으로 들어가 봅니다.
| 계층 | 검증 대상 | 가스 | 시간 |
|---|---|---|---|
npm test |
흥정 하한선 강제·경계값·주민 데이터 정합성·야간 조명 하한· 던전 확률표 대조·문 앞 기척(어느 것이든 열에 여덟 이상 · 문이 하나로 굳지 않는지)·마을 충돌(벽 통과·상호작용 도달)·주민 걸음· 소리·화면 임계 대조·공개 프로토콜 정합성·HUD 함정·재미 루프 (68건) | 0 — 체인 없음 | 2초 미만 |
npm run smoke:protocol |
PROTOCOL.md만 보고 짠 클라이언트로 룸 입장 —
매치메이킹·입장 확인·프레임 해독·15Hz 스냅샷 (colyseus.js 안 씀) |
0 — 로컬 서버 | ~10초 |
npm run smoke:boot |
실브라우저로 첫 방문자 동선 — 캔버스 부팅·노점·주민·온보딩 노출, 미처리 예외와 콘솔 에러 0건 (배포 게이트) | 0 — 읽기만 | ~30초 |
npm run test:local |
컨트랙트 10종 전체 — 에스크로·쿠폰·흥정·길드/던전·칭호·복주머니·공방· 비컨·프로필·v2 상태 이전, 그리고 장날·쿨다운·10분 창을 시간 여행으로 (54건) | 0 — 로컬 체인 | ~5초 |
npm run test:chain |
실제 배포본 10종 생존·Dojang·UP.ID (읽기 전용, --yes 필요) |
0 — 읽기만 | ~10초 |
| 개별 스모크 | 실거래가 정말 필요한 검증 (에스크로 실결제 등) | 든다 | 분 단위 |
로컬 계층의 핵심은 anvil을 chain-id 91342(GIWA Sepolia와 동일)로 띄운다는 점입니다. 코드의 체인 가드가 그대로 통과하고 같은 컨트랙트를 컴파일·배포해 같은 봇 코드를 돌리므로, 코드 경로가 프로덕션과 동일한 진짜 E2E입니다 — 가스만 무한할 뿐입니다. (GIWA 자체 노드는 500GB NVMe로 실제 테스트넷을 동기화하는 구조라 개발용 devnet으로는 맞지 않고, 같은 체인이라 가스도 그대로 듭니다.)
그리고 공짜보다 중요한 것이 시간 여행입니다. 테스트넷에서는 실제로 그 시각이 될 때까지 기다려야만 확인할 수 있던 것들 — 장날(토 21시 KST) 온기 2배, 도깨비 타격 쿨다운 30초, 모닥불 10분 창이 닫힌 뒤에야 수령 가능 — 을 초 단위로 점프해 검증합니다. 덤으로 컨트랙트(Solidity)와 클라이언트(TS)의 장날 판정이 표본 64개에서 일치하는지 대조합니다. 어긋나면 HUD는 "장날!"인데 실제로는 2배가 아닌 상태가 되는데, 그 불일치는 실시간 관찰로는 주 1회 1시간만 확인할 수 있습니다.
실제 실행 결과 보기 — npm run test:local (컨트랙트 10종 · 54건 · 가스 0)
아래 이미지는 scripts/render-run.mjs가 실행 출력을 그대로 SVG로 렌더한 것입니다 —
손으로 옮겨 적은 게 아니라 그때 그 실행의 표준출력입니다.
안전 불변식은 scripts/lib/haggle.mjs 한 곳에 모아 봇과 테스트가 공유합니다.
enforce()가 최종 관문이라 모델이 "공짜로 드릴게요"를 반환해도 차단되고,
그 케이스가 테스트에 들어 있습니다. 테스트가 실제로 회귀를 잡는지도 확인했습니다 —
과거에 겪었던 HUD pointer-events 버그를 다시 심으면 테스트가 실패합니다.
규칙 검증만으로는 "마을이 실제로 뜨는가"를 알 수 없습니다. 그래서 마지막에
npm run smoke:boot이 데모 빌드를 진짜 크롬으로 열어 첫 방문자 동선을
밟습니다 — 캔버스 부팅 → 노점·주민 등장 → 촌장의 부탁 노출 → 풍류 토글. 이때 두 가지가
요령입니다. 빈 localStorage로 엽니다 — 개발자 브라우저에는 온보딩
진행 기록이 남아 있어, 그대로 검사하면 "처음 온 사람에게만 나는 문제"를 영영 못 잡습니다.
그리고 공개 테스트넷 RPC에서 온 콘솔 에러는 일부러 걸러냅니다 — 남의 레이트리밋으로
게이트가 빨간불이 되면 사람은 곧 그것을 무시하게 되고, 그 순간 게이트는 죽은 장치입니다.
CI는 npm test → smoke:boot 순으로 돌리고, 어느 하나라도
실패하면 배포를 멈춥니다 — 깨진 데모가 올라가지 않습니다.
6GIWA Tech Use — 사용 중인 체인 기능
① Dojang Verified Address — isVerified(주소, 발행자 ID) 온체인 조회로
상인 인증 뱃지 표시. ② UP.ID(Upbit Web3 Names) — 주소→이름 역방향 조회로
아바타·노점 이름표. ③ 1초 블록 — 구매·비컨 확정 지연의 상한, 던전 주간
시드(블록 해시 핀)의 시간 해상도. ④ OP Stack L2 가스 단가 — 프레즌스 비컨
(이벤트 전용 tx, 회당 ~2µETH)을 실용적으로 만드는 전제. ⑤ 웹 기반이라 월렛
웹뷰 탑재가 가능한 구조.
7나중 작업 (P2) — 스케일 로드맵
현재 아키텍처는 "가치는 온체인, 존재감은 비컨" — 게임 서버 0으로 동작하고, 수십 명 규모의 밀도 있는 광장에 최적화돼 있다. 병목은 체인이 아니라 ① 공개 RPC 폴링 (유저 수 × 2.5초) ② 클라 렌더 O(N) ③ 전체 순회 뷰다. 다음 단계는 서버를 신뢰가 아닌 성능을 위해 되들이는 것이다.
- 읽기 캐시/인덱서 — 접속자가 늘면 모든 클라이언트가 RPC를 직접 폴링하는 구조는 레이트리밋에 막힌다. 계획: 경량 인덱서(Cloudflare Worker 또는 Colyseus 서버)가 노점·길드·비컨 이벤트를 구독·캐시하고, 클라이언트는 캐시를 우선 읽되 캐시가 죽으면 자동으로 RPC 직접 폴링으로 폴백 — 서버는 있으면 빠르고, 없어도 마을은 돈다.
- 실시간 승격(하이브리드 프레즌스) — 서버가 살아 있으면 이동·이모트를 Colyseus 릴레이(15Hz)로 승격해 부드러운 실시간, 서버가 없으면 지금의 2초 비컨+데드레커닝으로 자동 강등. 위치는 어차피 가치가 아니므로 어느 쪽이든 체인 기록과 무관하다.
- 비컨 압축·배치 — 다수 동시 접속 시 이동 델타 인코딩과 배치 전송으로 비컨 tx 수를 줄인다 (또는 세션키 기반 집계 릴레이).
- 던전 확장 — 문 결과 종류 추가(상인방: 쿠폰 할인, 보물방: 칭호 조각), 시즌제 아카이브(주간 기록의 온체인 보존), 길드 대항 이벤트.
- 실브랜드 기프티콘 — 쿠폰 소각(redeem) 이벤트를 기프티콘 애그리게이터 API 발급과 연결 — 온체인 사용 증빙이 곧 바코드 스캔.
- 분쟁 v2 — 현재는 구매자 신고(7일 연장) + 판매자 환불. 다음: 기한 내 미해결 건의 커뮤니티 중재(Dojang 인증 지갑 배심) 실험.