실습으로 진행했던 졸업 증명서를 응용하여 간단한 백신 증명서를 개발했다.
COOV를 쓰면서 DID 인증 방식이 어떻게 돌아가는지 궁금했기 때문에 개발 과정도 재미있었다.
이번 프로젝트를 통해 solidity에 대한 이해도를 높히고, 낯가리던 블록체인 기반 dApp 개발에 조금은 친해진 기분이다.
개발 내용과 개발 과정에서 배운점을 회고하면서 앞으로의 방향을 한 번 더 다잡아보자!
1. 요구사항
백신 증명서 내용
- 백신 제조사 (아스트라제네카/화이자/모더나)
- 접종차수 (1회/2회/3회)
- 접종일자 (2021.03.17)
- 상태 (미인증/인증완료)
* 요구사항 작성 중 COOV의 증명서 내용을 참고하였다.
백신 증명서 발급 주체 (Issuer)
- 정부
- 정부가 발급 권한을 위임한 기관
주요 기능
- 정부는 위임 기관을 추가 및 삭제할 수 있다
- 백신 증명서의 백신 제조사를 추가할 수 있다
- 접종 차수를 증가시킬 수 있다
- 백신 접종 여부를 확인할 수 있다
- 백신 제조사를 확인할 수 있다
- 백신 접종 완료 여부를 확인할 수 있다 (접종 완료 시점: 접종 후 2주)
구조체 및 생성자
// VC를 구현하기 위한 구조체
struct Credential{
uint256 id;
address issuer;
uint8 vaccineType; // 백신 제조사
uint8 shotNum; // 백신 접종 회차
string value; // credential에 포함되어야 하는 암호화된 정보 (JSON형태)
uint256 createDate;// 클레임한 시간
}
verifiable credential에 담길 정보들을 구조체로 구현한다.
민감한 정보는 보안을 위해 JSON 형태로 암호화하여 value 변수에 저장한다.
// 백신 타입을 uint8키와 string값으로 매핑
mapping(uint8 => string) private vaccineEnum;
// holder의 address를 키로, Credential구조체를 값으로 매핑
mapping(address => Credential) private credentials;
constructor() {
idCount = 1; // credential 순서를 위해 1부터 시작
vaccineEnum[0] = "Pfizer"; // 화이자
vaccineEnum[1] = "Moderna"; // 모더나
vaccineEnum[2] = "AstraZeneca";// 아스트라제네카
// vaccineEnum[3] = "Janssen"; // 얀센
}
* 얀센을 주석처리한 이유: '백신 제조사 추가 함수'가 정상적으로 작동하는지 확인하기 위함
2. 주요 기능 구현
1. 백신 증명서 발급
// issuer가 holder(_vaccineAddress)에게 credential을 발행하는 함수, onlyIssuer를 통과해야만 실행 가능
function claimCredential(address _vaccineAddress, uint8 _vaccineType, string calldata _value) onlyIssuer public returns(bool){
// credential 발행을 위해 credential 구조체 선언 (블록체인에 영향을 미쳐야 하므로 memory가 아닌 storagefh tjsdjs)
Credential storage credential = credentials[_vaccineAddress];
// credential.id가 0으로 처음 작성되는지 검증하고
require(credential.id == 0);
// credential 구조체에 맞게 idCount와 파라미터 데이터를 할당
credential.id = idCount;
credential.issuer = msg.sender;
credential.vaccineType = _vaccineType;
credential.shotNum = 1;
credential.value = _value;
credential.createDate = block.timestamp; // credential을 클레임한 시간 저장
idCount += 1;
return true;
}
2. 백신 제조사 추가
// 백신 타입 추가 함수
function addVaccineType(uint8 _type, string calldata _value) onlyIssuer public returns (bool) {
require(bytes(vaccineEnum[_type]).length == 0); // 백신 타입에 해당하는 타입이 없으면,
vaccineEnum[_type] = _value; // 새로운 타입(_type)을 키로 갖는 값(_value)을 할당한다
return true;
}
3. 백신 제조사 확인
// 해당 타입을 키로 갖는 백신 타입을 반환
function getVaccineType(uint8 _type) public view returns (string memory) {
return vaccineEnum[_type];
}
4. 백신 접종 차수 추가
// 백신 접종 차수 추가 함수
function addShot(address _vaccineAddress) onlyIssuer public returns (bool) {
require(credentials[_vaccineAddress].shotNum >= 1);
credentials[_vaccineAddress].shotNum += 1;
return true;
}
5. 백신 접종 여부 확인
// 백신 접종 여부 확인 함수
function isVaccinated(address _vaccineAddress) onlyIssuer public returns (bool) {
if(credentials[_vaccineAddress].shotNum >= 1)
return true;
else
return false;
}
6. 접종 2주 경과 확인
// 백신 접종 2주 경과 확인 함수
function checkTwoWeeks(address _vaccineAddress) onlyIssuer public returns (bool) {
uint256 memory vaccinatedDate = credentials[_vaccineAddress].createDate;
if((block.timestamp - vaccinatedDate) > 2 weeks)
return true;
else
return false;
}
7. 백신 증명서 내용 전체 확인
// holder 주소를 받아 VC 확인하는 함수
function getCredential(address _vaccineAddress) public view returns (Credential memory){
return credentials[_vaccineAddress];
}
* view
VS. pure
설명 | save 작업 | read 작업 | 공통점 | |
view | 데이터가 저장되지 않거나 변하지 않는 함수에 사용 | X | O | X | - 가스 비용이 들지 않음 - view/pure 명시하지 않을 경우보다 더 빠르게 접근 가능 (트랜잭션을 기다릴 필요가 없어서) |
pure | 데이터가 블록체인에 저장되지 않으며, & 블록체인으로부터 데이터를 읽지도 않는 함수에 사용 |
X | X |
3. 함수별 동작
1. issuers -> isIssuer -> isVaccinated -> claimCredential -> getCredential -> isVaccinated
배포만 한 초기 상태에서는 issuer만 있고 아직 클레임이 없기 때문에 백신 여부(isVaccined)의 결과는 false가 나온다.
claimCredential 함수를 실행해서 크레덴셜을 클레임하고, getCredential로 VC를 확인할 수 있다.
크레덴셜이 정상적으로 발급된 것을 볼 수 있으니, 당연히 백신 여부의 결과도 이제는 true를 반환한다.
2. addShot -> getCredential
addShot 함수를 실행해서 접종 차수를 증가시키고, getCredential의 output에서 접종 차수가 1에서 2로 변한 것을 확인할 수 있다.
3. getVaccineType -> addVaccineType
백신타입을 확인하는 기능과 새로운 백신타입을 추가하는 기능 모두 정상 작동하는 것을 볼 수 있다.
4. isIssuer -> addIssuer -> isIssuer -> issuers
새로운 issuer 가 잘 추가된 것을 확인할 수 있다.
6. transferOwnership -> etherscan log
log를 통해 owner의 주소가 바뀐것을 확인할 수 있다.
4. 개발 회고
# Keep
이번 프로젝트에서 DID 구현에 대한 '감을 익히는 것' 만큼은 꼭 얻고 가자고 생각했다. 초기 아이디어는 백신 유형이 있고 유형별로 또 백신 제조사가 있는 넓은 범위의 백신 증명서를 만드는 것이었다. 그러나 시간 관계상 복잡도를 줄여야 했고, 백신 유형은 COVID-19 하나로 의미를 좁히고 백신 제조사만 매핑하는 방식으로 구현했다. 보다 단순화한 덕분에 기초 원리에 더 집중할 수 있었다. Issuer 관리 기능은 앞서 실습으로 진행했던 졸업 증명서 프로젝트에서 사용한 함수를 그대로 사용했다. VC를 발급하고 확인하는 큰 틀 또한 동일하게 이어가는 한편, '접종 여부'와 더불어 '접종 완료 여부'를 추가적으로 확인하는 기능을 넣어 졸업 증명보다 기능을 확장하기도 했다. 그동안 COOV가 어떤 방식으로 구동되는지 궁금했는데, 얕게나마 따라하고 이해해볼 수 있어 좋았다.
solidity 문법의 기본기를 다시 다지게된 것도 좋았다. view와 pure의 차이를 다시 정리하였고, event와 emit을 연습했다.
개선안이 여러가지가 있을 때, 어떤 것을 선택할지 고민하는 시간도 꽤나 즐거웠다. owner를 변경하는 transferOwnership 함수에서 owner주소의 변경 전후 로그의 오류를 확인했는데, 개선안 두 가지가 있었다. 문제는 변경 전의 주소가 기록되어야할 자리에 변경 후의 주소가 기록되는 것이었다. 원인은 변경 전 주소를 가진 owner 변수 값을 변경 후 주소의 값으로 변경해놓고 owner를 그대로 emit해서 발생한 것이었다. 이를 해결하기위한 방안 하나는 변경 전 주소를 바로 집어넣는 것이었다. 변경 전 주소는 초기 owner의 주소이고, 이는 실행자의 주소이므로 msg.sender를 emit하는 것이다. 다른 하나는 transferOwnership 함수 안에서 변경 전 주소를 저장해둘 임시변수를 만들고, 이 임시변수를 emit하는 것이다. 가스비를 기준으로 비교했을 때 전자가 조금 더 저렴했기때문에 전자의 방식으로 코드를 수정했다.
# Problem
가장 큰 아쉬운 점은 크레덴셜의 verify 과정을 구현하지 못한 것이다. 블록체인 전반에 대한 개념을 깊이 습득하지 못한 것이 원인이었다. 이론을 실습으로 연결짓고 구현하는 경험이 부족했다. 사실 요근래 흥미가 떨어져서 의욕도 같이 떨어지고, 자연스레 공부량도 감소했었다. 그러니 구현능력이 떨어지는 건 당연한 결과였다. 그치만 과거는 과거고, 객관적인 나의 위치를 알게되니 오히려 더 자극이 되는 것 같다! 이번 프로젝트 주제는 내가 블록체인 분야 중에서 가장 관심있고 흥미있는 주제였는데, 그러다보니 확실히 공부에 다시금 흥미를 느끼게 됐다. 더 잘하고싶은 마음. 이제 의욕도 충전됐고, 조금씩 감이 잡히고 있는 것 같으니 verify 관련 개념을 보충하고 주말동안 구현을 시도해볼 것이다!
그리고 한 가지 삽질... 아무리 봐도 오타가 없는데 왜 자꾸 DeclarationError: Identifier not found or not unique
에러가 날까 했더니… contract CredentialBox 에 IssuerHelper 을 상속하는 것을 깜박해서 생긴 삽질이었다… 오타로 인한 에러가 종종 일어났어서 오타에만 집중했는데 이번엔 전체의 큰 틀을 살펴야하는 경우였다. 아무리 봐도 오타가 없다면 미시적으로 보지 말고 거시적으로 보기도 해야한다는 것을 배웠다. 앞으로는 추상 컨트랙트, 컨트랙트의 상속 관계 등도 유의하도록 하자! 숲을 잘 볼 줄 알아야 좋은 설계도 할 수 있으니까.
# Try
- COVID-19뿐만 아니라 더 다양한 백신 유형과 백신 유형별로 증명서를 발급받을 수는 없을까?
- 입력받은 값을 JWT토큰으로 암호화하고 이를 value값으로 넣어주는 일련의 과정을 자동화하면 좋지 않을까?
- 시간을 조금만 더 투자하면 verify 과정까지 구현해볼 수 있지 않을까?
구현 완료 후 claimCredential 함수의 동작을 확인할때, JWT토큰을 암호화하는 과정이 구현되지 않은 상태라 JWT 사이트에서 수동으로 암호화하고 그 결과값을 value값에 직접 넣어줬다. 번거롭고 실생활과 뒤떨어지는 방식이어서 아쉬웠다. 웹사이트 혹은 암호화 API를 추가적으로 만들어서 프로젝트를 확장시키면 좋을 것 같다. 또한 초기에 생각했던 백신 유형 확대 외에도 백신 교차접종, 백신 유효기간 설정을 추가해봐도 좋을 것 같다.
5. 소스코드
전체 코드와 롭스텐 테스트넷에 배포한 스마트 컨트랙트의 계약 주소는 링크로 걸어두었다.
전체 코드에는 블로그에는 적지 않은 Issuer 관리 (위임 기관 추가 및 삭제) 기능 등이 포함되어 있다.
Source Code | https://github.com/Ahreum0714/DID-VaccineCertification
Contract Address | https://ropsten.etherscan.io/address/0x1387701bdef3b456084a4a6f19d028efd6b74f90