다중서명(multi-signature)란?
•
우리가 지금까지 배운 디지털 서명은 하나의 개인키로 서명을 했었다.
•
다중서명은 서명할 때 여러개의 개인키로 서명하는 것을 말한다.
•
다중서명은 m of n 으로 표현되는데, 이는 n개의 서명 중 m개만 서명이 되면 승인한다는 뜻이다.
•
다중서명은 다음과 같은 기능을 수행할 수 있다.
◦
1 of 2 : 부부 소액 공동 계좌. 둘 중 한 사람의 서명으로도 거래 가능
◦
2 of 2 : 부부 공동 저축 계좌. 둘 모두 서명해야 거래 가능
◦
2 of 3 : 자녀를 위한 저축 계좌. 자녀가 부모 중 한 명의 승인을 받아야 거래 가능
◦
2 of 2 : 이중인증지갑. 하나는 PC에 하나는 모바일에 두 장치가 모두 있어야 거래 가능
◦
3 of 5 : 신뢰도가 낮은 기부. 5명의 위원이 키를 공유하고 3명 이상이 승인해야 거래 가능
◦
2 of 3 : 무신뢰 에스크로. 중개자를 믿을 수 없는 경우.
◦
2 of 3 : 조직 자금 관리. 이사회를 통해 거래 가능
◦
2 of 3 : 거래소의 고객 자금 보관. 고객이 한개의 키, 거래소가 한개의 키와 백업키를 통해 블랙리스트 관리, 거래소 보안성 강화에 활용
◦
2 of 3 : 분산 키 저장. 하나는 집에, 하나는 금고에, 하나는 친구에게 키를 배당하여 절도에 대응함.
◦
2 of 3 : 키 복구. 하나는 개인이, 하나는 운영사가, 하나는 제 3자가 보관하여 분실하더라도 키를 복원할 수 있음.
베어(bare) 다중서명
•
P2SH가 나오기 전에 사람들이 시도한 다중 서명 방법이다.
•
다중서명을 스크립트로 작성하기 위해서는 OP_CHECKMULTISIG(0xae) 명령어를 알아야 한다.
•
다음은 다중서명의 잠금 스크립트이다.
◦
2개의 공개키가 들어있다는 것을 알 수있다.
◦
51, 52를 통해 1-of-2 다중서명인 것을 확인할 수 있다.
•
다음은 해제 스크립트이다.
◦
OP_0은 OP_CHECKMULTISIG의 off-by-one 버그때문에 넣는 코드이다.
◦
이 코드는 스크립트 실행 과정에서 불필요하기 때문에 OP_0이 아닌 다른것을 써도 상관없다. (OP_0을 쓰는것이 관례다.)
◦
OP_CHECKMULTISIG의 작동 방식 때문에 발생한 버그이다.
•
이 스크립트는 아래와 같이 동작한다.
1.
잠금 스크립트와 해제 스크립트를 결합한다.
2.
처음 스택은 비어있다.
3.
OP_0이 먼저 스택에 올라간다.
4.
m개의 서명값이 스택에 올라간다.
5.
숫자 m과 n 그리고 공개키가 스택에 올라간다.
6.
OP_CHECKMULTISIG는 m+n+3개의 원소를 가져와 m-of-n서명을 검증한다.
구현: OP_CHECKMULTISIG
def op_checkmultisig(stack, z):
if len(stack) < 1:
return False
#n 추출
n = decode_num(stack.pop())
#스택 원소의 개수가 n+1보다 작으면 실패
if len(stack) < n + 1:
return False
sec_pubkeys = []
#배열에 공개키 할당
for _ in range(n):
sec_pubkeys.append(stack.pop())
m = decode_num(stack.pop())
if len(stack) < m + 1:
return False
der_signatures = []
#배열에 서명값 할당
for _ in range(m):
#서명값에서 SIGHASH 제외하고 서명값 저장
der_signatures.append(stack.pop()[:-1])
#버그 처리
stack.pop()
try:
#공개키 파싱
points = [S256Point.parse(sec) for sec in sec_pubkeys]
#서명값 파싱
sigs = [Signature.parse(der) for der in der_signatures]
#모든 서명을 돌면서
for sig in sigs:
#더이상 서명값이 없으면
if len(points) == 0:
LOGGER.info("signatures no good or not in right order")
return False
#모든 공개키를 돌면서
while points:
point = points.pop(0)
#서명 검증
if point.verify(z, sig):
break
# the signatures are valid, so push a 1 to the stack
stack.append(encode_num(1))
#스택에 남아있는 값 검사
except (ValueError, SyntaxError):
return False
return True
Python
복사
P2SH(Pay-to-Script Hash)
•
베어다중서명은 여러 개의 공개키와 서명값을 가지고 있어서 스크립트가 너무 길다.
•
복잡한 트랜잭션 스크립트의 사용을 간소화하기 위해 P2SH가 제안되었다.
•
P2SH는 스크립트의 해시코드에 지불한다는 의미이다.
◦
P2SH는 “이 해시코드와 일치하는 스크립트에 지불함”을 의미한다.
◦
스크립트의 해시코드를 추출하여 비트코인 주소로 사용하는 것이다.
◦
다양한 스크립트를 작성하고 해당 스크립트에 비트코인을 송금할 수 있다.
◦
보통은 다중서명 스크립트를 사용한다.
•
P2SH에서 스크립트 코드는 잠금스크립트를 해제 시 제공한다.
◦
UTXO의 용량 효율성을 줄일 수 있다.
◦
해제시에 제공된 스크립트 코드를 해시하여 잠금 스크립트의 해시값과 비교한다.
◦
스크립트 원본을 리딤 스크립트(redeemscript)라 한다.
◦
잠금을 해제하기 위해서는 리딤 스크립트를 해제자가 별도로 잘 보관해야 한다.
◦
이러한 점 때문에 도입에 많은 논쟁이 있었지만 현재는 잘 작동하고 있다.
◦
BIP-16에서는 리딤 스크립트를 처리하는 특별한 규칙을 제안하였다.
•
◦
BIP-16과 BIP-12
◦
BIP-12는 OP_EVAL 명령어를 통해 스크립트를 삽입하는 방법이다.
◦
OP_EVAL은 스택의 원소를 가져와서 리딤 스크립트 안에 넣고 구동하는 방식이다.
◦
이는 중간에 리딤 스크립트를 삽입하는 것 보다 직관적이다.
◦
하지만, 이는 비트코인 스크립트에 튜링완전성을 부여하는 문제가 있었다.
◦
따라서 BIP-12는 철회되고 OP_EVAL은 삭제되었다.
◦
그리고 스크립트가 중간에 삽입되는 형태의 BIP-16이 채택되었다.
•
잠금 스크립트는 다음과 같이 구성된다.
◦
스크립트 코드의 해시값만 들어있다.
•
해제 스크립트의 구성은 다음과 같다.
◦
두 개의 서명값과 리딤 스크립트가 들어있다.
•
리딤 스크립트의 구성은 다음과 같다.
◦
이 스크립트는 2-of-2 다중서명의 예시이다.
•
P2SH를 지원하는 노드는 리딤 스크립트가 명령어 집합에 들어가는 조건이 있다.
◦
스크립트에 네 개의 명령어가 남은 경우에는 동작하는 규칙이다.
◦
스택 위에 값이 1이 있다면 리딤 스크립트를 파싱하여 명령 집합으로 만든다.
•
P2SH의 구동 과정을 알아보자.
1.
잠금, 해제 스크립트가 합쳐진다.
2.
OP_0과 리딤 스크립트를 포함한 서명 데이터가 스택에 들어간다.
3.
리딤 스크립트의 해시값을 검사한다.
4.
해시값이 맞다면 다음과 같은 상태가 된다.
5.
BIP-16을 구현한 노드일 경우 스택 위에 1이 있다면 리딤 스크립트를 파싱한다.
6.
스택으로 리딤 스크립트를 하나씩 삽입하고 실행한다..
7.
2-of-2 다중서명을 검증한다.
구현 : P2SH
class Script:
#...생략
#Script 클래스의 evaluate 메소드를 수정한다.
def evaluate(self, z):
#... 생략
#커멘드를 별도의 공간에 담는다.
cmds = self.cmds[:]
stack = []
altstack = []
#cmds 변수가 빌 때 까지
while len(cmds) > 0:
#일단 하나를 먼저 꺼낸다.
cmd = cmds.pop(0)
#연산자이면
if type(cmd) == int:
#...생략
#연산자가 아니면
else:
#커맨드를 스택에 담는다.
stack.append(cmd)
#BIP-16의 조건 발동 여부 : 연산자의 길이가 3개 이고 0번이 OP_HASH160, 1번이 바이트형이고 길이가 160비트이며 2번이 OP_EQUAL인 경우
if len(cmds) == 3 and cmds[0] == 0xa9 and type(cmds[1]) == bytes and len(cmds[1]) == 20 and cmds[2] == 0x87:
#OP_HASH160 명령어를 꺼내고
cmds.pop()
#해시값을 꺼낸다.
h160 = cmds.pop()
#OP_EQUAL을 꺼낸다.
cmds.pop()
#해시값 추출
if not op_hash160(stack):
return False
stack.append(h160)
#해시값 검사
if not op_equal(stack):
return False
#1이 남아야 한다.
if not op_verify(stack):
LOGGER.info('bad p2sh h160')
return False
#리딤 스크립트를 가져와 길이와 함께 붙인다.
redeem_script = encode_varint(len(cmd)) + cmd
stream = BytesIO(redeem_script)
#리딤 스크립트를 파싱하고 명령어에 삽입한다.
cmds.extend(Script.parse(stream).cmds)
Python
복사
P2SH 주소
•
P2SH 주소는 P2PKH의 방식에 쓰이던 공개키의 해시값 대신 스크립트 코드의 해시값을 사용한다.
•
접두부는 0x05를 사용한다. (테스트넷은 0xc4)
•
P2PKH와 마찬가지로 Base54Check 인코딩을 한다.
•
인코딩을 거치면 P2SH 주소는 ‘3’으로 시작한다.
P2SH 서명 검증
•
P2PKH의 경우 1개의 공개키로 1개의 서명을 검증하지만, P2SH는 복수개의 공개키를 사용한다.
•
P2SH에서 서명을 위한 z(메시지의 해시)는 다음과 같이 구할 수 있다.(SIGHASH_ALL을 가정하자)
1.
다음은 P2SH 트랜잭션의 예시이다.
2.
해제스크립트를 지운다.
3.
해제 스크립트 자리에 리딤 스크립트를 삽입한다.
4.
해시유형을 덧붙인다.
5.
SHA-256 해시값을 구한다.
6.
결과값을 z로 사용한다.
•
P2PKH에서 잠금 스크립트를 넣는 자리에 리딤 스크립트를 삽입하는 것이 다르다.