ERC-2098 서명 압축 표현 형식 (Compact Signature Representation)

Final · 서명·인증 · 2019-03-14 작성

이더리움 서명을 65바이트가 아닌 64바이트로 담는 방법을 정합니다. 서명의 세 값 가운데 yParity를 s 값의 맨 윗 비트에 끼워 넣어 r와 yParityAndS 두 값만으로 서명을 나타냅니다.

왜 필요한가

이더리움에서 서명은 r, s, yParity 세 값으로 이뤄지며 모두 합쳐 65바이트입니다. 그런데 체인 위에서는 데이터를 256비트(32바이트) 단위로 맞춰 다루기 때문에, 65바이트 서명을 그 단위에 맞추면 0으로 채운 31바이트가 끼어들어 96바이트가 됩니다. 트랜잭션을 RLP 방식으로 인코딩할 때도 yParity 값이 평균 1.5바이트를 따로 차지합니다. 외부 소유 계정(EOA)이 보낸 트랜잭션의 출처를 확인할 때는 물론, 메타트랜잭션이나 다중서명 컨트랙트처럼 체인 위에서 서명을 검증하는 경우에도 이 낭비가 그대로 반복됩니다. 결과적으로 트랜잭션은 필요 이상으로 커지고 가스 비용도 늘어나며 클라이언트 코드는 세 값을 따로따로 다뤄야 합니다.

구현해야 하는 것

심층 설명

서명을 이루는 세 값 r, s, yParity

secp256k1 곡선의 서명은 r, s, yParity 세 값으로 이뤄집니다. r는 곡선 위 점의 x 성분이며 이 값에서 y를 계산할 수 있습니다. s는 개인키로 서명할 때 풀어낸 도전 값(challenge solution)입니다. 타원곡선은 대칭이라 x 하나에 가능한 y가 둘이므로, 둘 중 어느 쪽을 뜻하는지 홀짝(parity)으로 알려 주는 값이 yParity입니다. 서명과 서명한 다이제스트가 있으면 서명자의 공개키를 계산할 수 있습니다. 이 성질을 이용해 외부 소유 계정이 보낸 트랜잭션의 출처를 확인하거나 메타트랜잭션과 다중서명 컨트랙트에서 체인 위 검증을 합니다.

압축을 가능하게 하는 두 가지 관찰

첫째, yParity는 언제나 0 아니면 1입니다. 관례적으로는 27과 28을 써 왔는데, 비트코인에서 쓰는 다른 이진 접두사와 겹치지 않도록 고른 값입니다. 둘째, s의 맨 윗 비트는 항상 0입니다. Homestead에서 도입된 제약(EIP-2)에 따라 정규 서명(canonical signature)만 허용되며 정규 서명은 음수 값을 막기 위해 해의 홀짝을 뒤집기 때문입니다. 이 두 사실을 합치면 s의 맨 윗 비트는 늘 비어 있고 yParity는 1비트면 충분하므로, s의 맨 윗 비트를 가로채 yParity를 저장할 수 있습니다.

비트 배치와 변환 식

압축 서명은 [256비트 r 값][1비트 yParity 값][255비트 s 값] 순서로 배치합니다. 원문의 파이썬 예시는 yParity가 27·28이 아니라 0·1로 정규화돼 있다고 가정합니다. 압축할 때는 to_compact(r, s, yParity)가 r는 그대로 두고 yParityAndS = (yParity << 255) | s를 만듭니다. 되돌릴 때는 to_canonical(r, yParityAndS)가 s = yParityAndS & ((1 << 255) - 1)로 아래 255비트를 꺼내고 yParity = yParityAndS >> 255로 맨 윗 비트를 꺼냅니다. 원문은 이 방식이 클라이언트와 Solidity 양쪽에서 만들고 푸는 과정이 단순해 쉽고 직관적으로 지원할 수 있다고 설명합니다.

줄어드는 크기

일반 서명은 65바이트이고 256비트 워드에 맞추면 0으로 채운 31바이트가 들어가 96바이트가 됩니다. RLP로 인코딩한 트랜잭션에서는 yParity가 평균 1.5바이트를 더 차지합니다. 압축 서명은 64바이트이며 워드에 맞춰도 그대로 64바이트이고 RLP 트랜잭션에서는 yParity 몫인 1.5바이트를 아낍니다. 원문은 이를 통해 클라이언트 코드의 트랜잭션 처리를 단순하게 하고 가스 비용과 트랜잭션 크기를 줄이는 것을 목표로 삼습니다.

기존 서명과 구별되는 방식

압축 표현은 기존 정규 서명과 충돌하지 않습니다. 압축 표현은 r와 yParityAndS 두 값으로 이뤄지고 길이가 64바이트인 반면, 정규 서명은 r, s, yParity 세 값으로 이뤄지고 길이가 65바이트입니다. 값의 개수와 길이가 서로 다르므로 두 형식이 섞일 일이 없고 원문은 이 차이를 근거로 하위 호환성에 문제가 없다고 봅니다. 기존 65바이트 서명을 그대로 쓰는 코드와 64바이트 압축 서명을 쓰는 코드가 나란히 있어도 구분할 수 있습니다.

테스트 케이스로 보는 변환

원문은 같은 개인키로 두 메시지에 서명한 예를 제시합니다. 메시지 「Hello World」의 서명은 v가 27이므로 yParity가 0이고 압축 서명의 yParityAndS는 s와 똑같이 0x7e865ad0으로 시작하는 값입니다. 메시지 「It's a small(er) world」의 서명은 v가 28이므로 yParity가 1입니다. s의 맨 윗 비트가 1로 바뀌어 0x139c6d6b로 시작하던 s가 압축 서명에서는 0x939c6d6b로 시작하는 yParityAndS가 됩니다. 두 경우 모두 r는 바뀌지 않습니다.

참조 구현과 상태

ethers.js 라이브러리는 v5에서 분할 서명(split signature)의 비공식 속성 sig._vs로 이 표현을 지원합니다. 다만 원문은 이를 내부 속성으로 보아야 하며 커뮤니티의 판단이나 이 EIP의 변경에 따라 바뀔 수 있다고 밝힙니다. 공식 API가 아니므로 이 속성에 기대는 코드는 그 점을 전제로 해야 합니다. 보안 고려사항 절에서는 이 EIP가 추가로 들여오는 보안 우려는 없다고 적고 있습니다. 상태는 Final로 확정된 표준입니다.

ERC 원문 · ← ERCs