ERC-4519 실물 자산 연동 NFT (Non-Fungible Tokens Tied to Physical Assets)
Final · NFT·멀티토큰 · 2021-12-03 작성
사물인터넷(IoT) 기기 같은 실물 자산에 묶인 NFT의 표준 인터페이스입니다. 자산이 자기 이더리움 주소로 서명하고 소유자·사용자와 상호 인증해 연결이 진짜인지 검증할 수 있게 합니다.
왜 필요한가
가장 널리 쓰이는 NFT 표준인 EIP-721은 토큰의 소유권만 기록합니다. 누가 자산을 실제로 쓰는지(사용권)는 다루지 않고 자산 자체의 이더리움 주소도 기록하지 않습니다. 그래서 IoT 기기 같은 스마트 자산을 NFT로 표현해도 그 토큰이 정말 그 기기와 연결되어 있는지 확인할 방법이 없고 기기를 빌려 쓰는 사용자를 소유자와 구분해 관리할 수도 없습니다. 이 표준은 NFT에 자산과 사용자의 이더리움 주소를 담고 자산이 스스로 메시지와 트랜잭션에 서명할 수 있게 하며 소유자·사용자와 자산이 서로를 인증해 안전한 통신 채널을 만들도록 해서 이 문제를 풉니다. 소유자와 사용자가 바뀌어도 자산의 신뢰성을 계속 추적할 수 있습니다.
구현해야 하는 것
event UserAssigned(uint256 indexed tokenId, address indexed _addressUser);: 토큰이 새 사용자에게 배정되거나 사용자가 바뀔 때 발생합니다. _addressUser가 0이면 배정된 사용자가 없다는 뜻입니다.event UserEngaged(uint256 indexed tokenId);: 사용자와 자산이 상호 인증을 성공적으로 마쳐 안전한 통신 채널을 공유함을 증명했을 때 발생합니다.event OwnerEngaged(uint256 indexed tokenId);: 소유자와 자산이 상호 인증을 성공적으로 마쳐 안전한 통신 채널을 공유함을 증명했을 때 발생합니다.event TimeoutAlarm(uint256 indexed tokenId);: 토큰의 타임스탬프가 정해진 타임아웃 안에 갱신되지 않아 만료가 확인됐을 때 발생합니다.function setUser(uint256 _tokenId, address _addressUser) external payable;: 토큰 소유자만 호출해 새 사용자를 배정합니다. addressAsset이 정의되어 있으면 상태를 waitingForUser로, 아니면 userAssigned로 바꿉니다.function startOwnerEngagement(uint256 _tokenId, uint256 _dataEngagement, uint256 _hashK_OA) external payable;: 소유자가 자산과의 상호 인증을 시작합니다. 키 합의용 공개 데이터와 공유 비밀의 해시를 토큰에 저장합니다.function ownerEngagement(uint256 _hashK_A) external payable;: 자산이 자기가 계산한 비밀의 해시를 보내 소유자 인증을 끝냅니다. 저장된 hashK_OA와 같으면 상태가 engagedWithOwner로 바뀝니다.function startUserEngagement(uint256 _tokenId, uint256 _dataEngagement, uint256 _hashK_UA) external payable;: 사용자가 자산과의 상호 인증을 시작합니다. 키 합의용 공개 데이터와 공유 비밀의 해시를 토큰에 저장합니다.function userEngagement(uint256 _hashK_A) external payable;: 자산이 자기가 계산한 비밀의 해시를 보내 사용자 인증을 끝냅니다. 저장된 hashK_UA와 같으면 상태가 engagedWithUser로 바뀝니다.function checkTimeout(uint256 _tokenId) external returns (bool);: 누구나 호출해 타임아웃이 지났는지 확인합니다. 지났으면 TimeoutAlarm 이벤트를 발생시키고 true를 돌려줍니다.function setTimeout(uint256 _tokenId, uint256 _timeout) external;: 토큰 소유자만 타임아웃 값을 설정합니다. 토큰 상태가 engagedWithOwner, waitingForUser, engagedWithUser일 때만 가능합니다.function updateTimestamp() external;: 토큰에 묶인 자산만 호출해 자기 타임스탬프를 갱신하고 타임아웃 경보를 피합니다.function tokenFromBCA(address _addressAsset) external view returns (uint256);: 자산의 주소로 그 자산에 묶인 토큰의 tokenId를 조회합니다.function ownerOfFromBCA(address _addressAsset) external view returns (address);: 자산의 주소로 그 자산에 묶인 토큰의 소유자를 조회합니다.function userOf(uint256 _tokenId) external view returns (address);: tokenId로 그 토큰의 사용자를 조회합니다.function userOfFromBCA(address _addressAsset) external view returns (address);: 자산의 주소로 그 자산에 묶인 토큰의 사용자를 조회합니다.function userBalanceOf(address _addressUser) external view returns (uint256);: 한 사용자에게 배정된 토큰이 몇 개인지 조회합니다.function userBalanceOfAnOwner(address _addressUser, address _addressOwner) external view returns (uint256);: 특정 소유자의 토큰 중 그 사용자에게 배정된 토큰이 몇 개인지 조회합니다.
심층 설명
두 개의 주소 속성과 토큰 상태
NFT는 자산의 이더리움 주소인 addressAsset과 사용자의 주소인 addressUser를 둘 수 있습니다. 둘 다 선택 속성이지만 적어도 하나는 써야 합니다. addressUser만 쓰면 토큰은 notAssigned와 userAssigned 두 상태를 오가며 생성·이전·배정 해제 시 notAssigned가 되고 유효한 사용자에게 배정되면 userAssigned가 됩니다. addressAsset만 쓰면 waitingForOwner와 engagedWithOwner 두 상태로, 생성되거나 새 소유자에게 이전되면 waitingForOwner가 되고 자산과 소유자의 상호 인증이 끝나면 engagedWithOwner가 됩니다. 둘 다 쓰면 waitingForOwner, engagedWithOwner, waitingForUser, engagedWithUser 네 상태로 자산이 소유자·사용자와 연결됐는지를 나타냅니다.
타원곡선 기반 키 합의
소유권 이전을 마치려면 새 소유자가 자산과 상호 인증을 해야 합니다. 자산과는 오프체인으로, 토큰과는 온체인으로 진행합니다. 소유자는 타원곡선 secp256k1과 기본 원소 P로 비밀키 SK_O_A와 공개키 PK_O_A = SK_O_A * P를 만들고 자산의 공개키 PK_A로 공유키 K_O = PK_A * SK_O_A를 계산합니다. 자산은 자기 비밀키로 K_A = SK_A * PK_O_A를 계산하는데, 수식상 두 값은 같습니다. 토큰은 합의에 필요한 공개 데이터를 dataEngagement에, 공유키의 해시를 hashK_OA와 hashK_UA에 저장하며 자산·소유자·사용자는 올바른 공유 비밀키를 쓰고 있는지 확인해야 합니다. 인증마다 새 세션 키가 만들어지므로 소유자와 사용자가 바뀌어도 자산의 신뢰성을 추적할 수 있습니다.
소유자 인증 함수의 동작 조건
startOwnerEngagement는 addressAsset이 정의되어 있고 토큰 상태가 waitingForOwner일 때 토큰 소유자만 호출할 수 있습니다. 상태는 바꾸지 않고 _dataEngagement와 _hashK_OA만 토큰에 저장합니다. ownerEngagement는 상태가 waitingForOwner이고 dataEngagement가 0이 아닐 때 토큰에 묶인 자산만 호출할 수 있습니다. 토큰에 저장된 hashK_OA와 자산이 보낸 hashK_A를 비교해 같으면 상태를 engagedWithOwner로 바꾸고 dataEngagement를 0으로 만든 뒤 OwnerEngaged 이벤트를 발생시킵니다. 자산은 이 이벤트를 받고 자기 동작 모드를 engagedWithOwner로 바꿉니다.
사용자 배정과 사용자 인증
setUser는 토큰 소유자만 호출할 수 있습니다. addressAsset이 정의된 경우 토큰 상태가 engagedWithOwner, waitingForUser, engagedWithUser 중 하나여야 하고 호출 후 상태는 waitingForUser가 됩니다. addressAsset이 없으면 상태는 userAssigned가 됩니다. 두 경우 모두 addressUser가 _addressUser로 설정됩니다. 자산이 체인을 조회해 자기 NFT가 waitingForUser 상태임을 확인하면 동작 모드를 waitingForUser로 바꾸고 소유자와 했던 것과 같은 상호 인증을 사용자와 진행합니다. startUserEngagement는 addressAsset과 addressUser가 정의되고 상태가 waitingForUser일 때 사용자만 호출할 수 있고 userEngagement는 자산만 호출해 해시가 일치하면 상태를 engagedWithUser로 바꾸고 UserEngaged 이벤트를 발생시킵니다. 그때부터 사용자가 자산을 사용할 수 있습니다.
타임스탬프와 타임아웃
토큰에는 자산이 마지막으로 연결을 확인한 시각을 기록하는 timestamp 속성과, 자산이 다시 연결을 증명해야 하는 최대 지연 시간인 timeout 속성이 있습니다. 악의적인 소유자나 사용자가 자산을 무한정 쓰는 일을 막기 위한 장치입니다. 자산이 updateTimestamp를 호출하면 컨트랙트는 block.timestamp를 사용해야 하는데, 이 값은 유닉스 기준 초 단위이므로 timeout도 초 단위로 지정해야 합니다. checkTimeout은 누구나 호출해 만료 여부를 확인할 수 있고 만료됐으면 TimeoutAlarm 이벤트가 발생합니다. setTimeout은 토큰 상태가 engagedWithOwner, waitingForUser, engagedWithUser일 때 소유자만 호출할 수 있습니다.
조회 함수와 EIP-721 호환성
자산 주소로 토큰을 찾는 tokenFromBCA, 소유자를 찾는 ownerOfFromBCA, 사용자를 찾는 userOfFromBCA와 userOf, 사용자에게 배정된 토큰 수를 세는 userBalanceOf와 userBalanceOfAnOwner 같은 읽기 전용 함수를 제공해 누구나 체인에서 자산과 토큰의 관계를 조회할 수 있습니다. 이 표준은 가장 널리 쓰이는 NFT 표준인 EIP-721을 확장한 것으로, 선택 확장인 IERC721Metadata, IERC721Enumerable과도 완전히 호환됩니다. 이 인터페이스의 EIP-165 식별자는 0x8a68abe3입니다. 이 제안은 Final 상태로 확정된 표준입니다.
쓸 때 주의할 점
- 자산 주소는 자산만 아는 비밀에서 생성: 실물 자산은 완전히 무작위한 방식으로 스스로 이더리움 주소를 만들 수 있어야 하고 주소의 바탕이 되는 비밀은 자산만 알아야 합니다. 그래야 신원 도용을 막고 자산이 진짜임을 증명할 수 있습니다.
- 토큰 생성은 제조사만: 이를 보장하기 위해 자산에 연결된 토큰은 그 자산의 제조사만 만들 수 있게 하는 것이 권장됩니다.
- 비밀을 저장하지 말고 재구성: IoT 기기라면 펌웨어가 비밀을 공유하거나 수정할 수 없어야 합니다. 비밀을 저장하는 대신 물리적 복제 방지 기능(PUF)의 보조 데이터 같은 민감하지 않은 정보로부터 비밀을 재구성하는 방식이 권장됩니다.
- 다른 키 교환 방식과 공존 가능: 타원곡선 기반의 안전한 키 교환 프로토콜이 제안되어 있지만 토큰은 다른 종류의 키 교환 방식과도 함께 쓸 수 있도록 열려 있습니다.