URI·클립보드·QR: 암호화폐 주소는 전달 과정에서 이렇게 깨진다
주소는 지갑에 도착할 때까지 URI, 클립보드, QR 세 인코더를 거칩니다. 각각이 어떻게 깨뜨리는지 실측하고 전송 전에 확인할 점을 정리했습니다.
주소는 건네주는 '물건'이 아닙니다. 전송 버튼을 누르기 전에 이미 여러 번 인코딩되고, 복사되고, 렌더링되고, 스크린샷으로 찍히고, 다시 디코딩된 문자열입니다. 교환 주문 페이지에서 그 문자열이 나가는 문은 딱 세 개입니다. 복사 버튼, 200픽셀 QR 코드, 그리고 선택 가능한 텍스트. 셋은 각각 다르게 깨지고, 깨져도 보이지 않습니다. 깨진 버전도 여전히 주소처럼 보이기 때문입니다. 우리는 이 셋을 실제 사이트에서 측정했습니다. 클립보드가 무엇을 남기는지, QR이 무엇을 인코딩하는지, 아홉 가지 주소 형식이 열두 가지 변형에 어떻게 반응하는지. 대가는 구체적입니다. 오늘 18:06 UTC, 100 USDT-TRC20은 0.13937044 XMR로 견적되었습니다. 한 글자 빠진 수취 주소는 주문 실패가 아니라, 목적지 없는 결제 완료 주문이 됩니다. 같은 시각 카탈로그는 434개 자산·62개 네트워크였고, 그중 6개는 메모가 필수입니다. (XMR→USDT 출구 루트)
먼저 URI입니다. 금액을 실을 수 있는 유일한 통로이기 때문입니다. BIP-21의 문법은 bitcoin:<주소>?amount=<십진 BTC>입니다. 금액은 십진 BTC, 최대 여덟 자리, 사토시 단위는 안 되고, 클라이언트는 모르는 파라미터를 거부가 아니라 무시해야 합니다. EIP-681은 ethereum:<주소>@<체인 ID>/transfer?address=<수취인>&uint256=<최소 단위>입니다. 여기서 금액은 토큰 최소 단위의 정수입니다. 1 USDT는 1000000이지 1이 아니며, value=2.014e18 같은 지수 표기가 명시적으로 허용됩니다. 모네로는 monero:<주소>?tx_amount=<십진 XMR>&tx_payment_id=<64자리 hex>이고, wallet2.cpp의 파서는 모르는 파라미터를 unknown_parameters에 쌓아두고도 성공을 반환합니다. 이 한 줄이 고장의 전부입니다. tx_amount를 amount로 쓴 URI는 거부되지 않고 '금액 없음'으로 접수되며, 금액 입력은 사람에게 돌아옵니다. (XMR→USDT 출구 루트)
사이트 검증기로 아홉 형식 × 열두 변형, 총 90칸을 돌렸습니다. URI 접두사가 붙은 것은 모든 체인에서 거부됐고, 사유도 "not a Bitcoin address (expect bc1…, 1… or 3…)"처럼 소박했습니다. 붙여넣기 칸으로서는 옳은 판단입니다. 교환소는 자기가 만들지 않은 문자열 안의 금액을 실행해서는 안 됩니다. URI의 금액은 지시이지 목적지가 아니기 때문입니다. 대가는 편도라는 점입니다. 지갑이나 익스플로러가 bitcoin:bc1q…?amount=0.01을 건네도 이 칸은 받지 않습니다. scheme과 쿼리를 떼고 경로만 남기세요. 반대로 우리 QR은 맨 주소만 인코딩합니다. 따라서 스캔한 코드가 틀린 금액을 담을 수 없고, 맞는 금액도 담지 않습니다.
다음은 클립보드입니다. 셋 중 가장 신뢰할 수 없고 가장 많이 쓰입니다. 복사 버튼은 navigator.clipboard.writeText를 호출하고, 오류를 삼키는 try/catch로 감쌉니다. 보안 컨텍스트가 아니거나 권한이 거부되면 아무것도 복사되지 않고 아무 안내도 나오지 않습니다. 검증기는 비교 전에 trim을 하므로 네 가지 흔한 잔여물이 조용히 구제됩니다. 끝의 개행, 끝의 공백, 양쪽의 줄바꿈 없는 공백(U+00A0), 앞의 BOM(U+FEFF)이고, 이 넷은 아홉 형식 전부에서 통과했습니다. 통과하지 못한 것은 셋입니다. 제로 폭 공백(U+200B), 오른쪽에서 왼쪽 표시(U+200F), 소프트 하이픈(U+00AD)은 중간에 넣든 끝에 붙이든 전 형식에서 거부됐습니다. 그리고 이것들이야말로 PDF, 채팅 앱, 양쪽 정렬된 웹페이지에서 복사할 때 가장 잘 끼어드는 문자입니다. 두 번째 함정은 대소문자입니다. bech32 전체 대문자는 합법이라 통과하고, 혼합 대소문자는 체크섬에서 떨어지며, 모네로 주소 전체 대문자는 즉시 거부되고, 대문자 X를 쓴 0X는 EVM 주소가 아닙니다.
QR은 별도 절이 필요합니다. 기하가 조용히 성패를 결정하기 때문입니다. 주문 페이지는 react-qr-code를 size=200, level 미지정으로 렌더링합니다. 즉 라이브러리 기본값인 L, 복원력 약 7%입니다. 이 컴포넌트는 모듈만 그리고 viewBox를 모듈 수로 잡으므로 코드 자체에는 콰이어트 존이 없습니다. 흰 여백은 컨테이너의 16픽셀 패딩입니다. 200 CSS 픽셀에서 그 여백은 29모듈 코드에 2.32모듈, 33에 2.64, 37에 2.96, 41에 3.28입니다. 규격은 4를 요구합니다. 모듈 크기는 6.90픽셀에서 4.88픽셀까지 내려갑니다. 106자라는 길이도 우연이 아닙니다. 버전 5의 L 바이트 용량과 정확히 같아서, 95자 모네로 주소와 106자 통합 주소가 모두 37모듈에 들어가고, 한 글자만 늘거나 level을 M으로 올리면 41로 뛰어오릅니다.
그리고 기하를 믿지 않고 실제로 디코딩했습니다. 200픽셀 실제 레이아웃을 네 배로 렌더링하고 열화시킨 뒤 jsQR 1.4.0으로 칸당 20회 무작위 흔들림을 주어 읽힌 결과는 깔끔한 곡선이었습니다. 116픽셀 폭, 즉 렌더링 크기의 절반까지는 스물네 조합이 매번 정확히 읽혔습니다. 품질 40 JPEG, 2도 회전, 서브픽셀 이동을 겹쳐도 그랬습니다. 93픽셀에 0.8픽셀 블러, JPEG 40, 3도 회전에서는 짧은 코드가 버텼습니다. 29모듈 20/20, 33모듈 19~20/20입니다. 긴 코드는 무너졌습니다. 37모듈은 9/20과 8/20, 41모듈은 2/20과 0/20으로 떨어졌습니다. 정정 레벨을 L에서 M으로 올리면 이 긴 코드들은 오히려 나빠졌습니다. 같은 200픽셀에 네 개 많은 모듈을 밀어 넣어야 하기 때문입니다. 반면 폭의 18%를 덮는 흰 원을 치우쳐 놓은 경우는 살아남았습니다. 짧은 코드 16~19/20, 긴 코드 20/20으로, 잉여 면적이 큰 쪽이 강했습니다.
마지막 구멍은 기하가 아니라 구조입니다. QR이 인코딩하는 것은 입금 주소뿐입니다. 컴포넌트에 넘어가는 값이 그것 하나이기 때문입니다. 메모는 그 아래 별도 복사 행에 있습니다. 카탈로그 434개 자산 중 6개가 메모 필수입니다. XRP, XLM, HBAR, ATOM, STX, NEIRO-eth이고, 공용 입금 주소가 이 구멍을 넓힙니다. 태그로 라우팅하는 체인에서는 여러 주문이 한 주소를 공유하므로, QR에서 멈춘 사람은 태그 없이 보내게 됩니다. XRP에서 그것은 지연이 아니라 풀이 귀속시킬 수 없는 입금입니다. 코드를 보면 모양도 그대로입니다. QR은 depositAddress에서, 메모는 depositExtraId에서, 서로 다른 요소로 그려지고 스캔 가능한 것은 하나뿐입니다.
그래서 실무 절차는 짧습니다. 주소를 복사해 붙여넣은 뒤, 보내기 전에 앞 네 글자와 뒤 네 글자를 화면과 대조하세요. 이 한 동작이 잘림과 글자 치환을 한 번에 잡습니다. 주소 칸에 URI를 붙여넣지 말고, 자기가 만들지 않은 URI의 금액을 받아들이지 마세요. QR을 읽는다면 금액은 0으로 두고 주문 페이지에서 다시 입력해 여덟 자리를 모두 맞추세요. 0.13937044는 0.1394가 아니고, 100 USDT 주문에서 그 차이는 도착액의 약 0.3%입니다. 메모가 필요한 자산은 아예 스캔하지 말고 주소와 메모를 따로 복사하고, 메모는 두 번 확인하세요. 체크섬이 없는 유일한 칸이기 때문입니다. 마지막으로 QR을 스크린샷해 채팅으로 넘기는 행동이 바로 93픽셀 사례를 현실로 만듭니다. 이미지는 남고, 주소는 남지 않을 수 있습니다. (XMR→USDT 출구 루트)
특별한 이야기가 아닙니다. 인코더 셋, 고장 유형 셋, 그 사이에서 어디가 깨졌는지 볼 수 없는 사람 하나. URI는 금액을 조용히 떨어뜨리고, 클립보드는 폭 없는 문자를 싣고, QR은 200픽셀 상자에 37모듈을 깝니다. 해법은 더 좋은 소프트웨어만이 아니라, 이 문자열이 어느 문으로 나왔는지 알아서 그 문이 깨뜨릴 수 있는 부분을 확인하는 것입니다. 자기가 관리하는 지갑에서 보내고, 환불 주소는 같은 체인에 두세요. 하나만 기억한다면 이렇게 기억하세요. 화면의 주소와 지갑의 주소는 대조하기 전까지 서로 다른 객체입니다. (모네로 확인 대기)