4️⃣

다중서명

다중서명(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-12였다.
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에서 잠금 스크립트를 넣는 자리에 리딤 스크립트를 삽입하는 것이 다르다.

참고문헌