def sig_hash(self, input_index): """ 해제 스크립트에 서명을 포함시켜 트랜잭션을 변형 step1 : 해제 스크립트 삭제 - 서명 검증시 트랜잭션의 해제 스크립트 삭제 - 서명 생성시 동일 - 입력이 여러 개인 경우, 해당 입력의 해제 스크립트 삭제 step2 : 이전 트랜잭션 출력의 잠금 스크립트를 입력의 해제 스크립트에 삽입 - 트랜잭션 입력에 대해 TxIn.script_pubkey를 사용하여 해제 스크립트 입수 가능 step3 : 해시 유형 추가 - 서명해시를 구하기 위한 메시지에 포함시킬 필드 결정 - SIGHASH_ALL : 현재 입력과 다른 모든 입출력을 모두 포함 step4 : 해시 계산 - 최종 변경된 트랜잭션의 hash256 해시값 계산 - 32바이트 빅엔디언 정수로 변환 :param input_index: 현재 입력에 대한 인덱스 :return: 변형된 트랜잭션의 서명해시 i.e 서명을 포함시킨 직렬화 정보 """ # 버전 : 4바이트 리틀 엔디언 result = int_to_little_endian(self.version, 4) # 이전 트랜잭션 정보 : 이전 트랜잭션 개수 및 이전 트랜잭션들의 정보 # 서명을 포함시키기 위해 serialize()와 다르게 직렬화 result += encode_varint(len(self.tx_inputs)) for idx, tx_in in enumerate(self.tx_inputs): # 현재 입력인 경우 if idx == input_index: # 해당 트랜잭션 해시값에 대응되는 트랜잭션의 잠금 스크립트로 스크립트 대체 tx_in_copy = TxIn(prev_tx=tx_in.prev_tx, prev_index=tx_in.prev_index, script_sig=tx_in.script_pubkey(self.testnet), sequence=tx_in.sequence) result += tx_in_copy.serialize() # 다른 입력 else: # 해제 스크립트는 포함시키지 않는다 tx_in_copy = TxIn(prev_tx=tx_in.prev_tx, prev_index=tx_in.prev_index, sequence=tx_in.sequence) result += tx_in_copy.serialize() # 트랜잭션 출력 : 출력 개수 및 출력들의 정보 result += encode_varint(len(self.tx_outputs)) for tx_output in self.tx_outputs: result += tx_output.serialize() # 록타임 : 4바이트의 리틀엔디언 result += int_to_little_endian(self.locktime, 4) # 해시 유형 덧붙임 : SIGHASH_ALL 사용함 result += int_to_little_endian(SIGHASH_ALL, 4) # 서명 해시 생성 result256 = hash256(result) sig_message_hash = int.from_bytes(result256, "big") return sig_message_hash
def exercise4(): from io import BytesIO from ecc.secp256k1 import S256Point, Signature from ecc.utils import encode_varint, hash256, int_to_little_endian from ecc.transaction import Tx, SIGHASH_ALL # 문제 : 주어진 p2sh 다중 서명 스크립트와 서명이 주어질 때, 해당 서명 검증 hex_tx = '0100000001868278ed6ddfb6c1ed3ad5f8181eb0c7a385aa0836f01d5e4789e6bd304d87221a000000db00483045022100dc92655fe37036f47756db8102e0d7d5e28b3beb83a8fef4f5dc0559bddfb94e02205a36d4e4e6c7fcd16658c50783e00c341609977aed3ad00937bf4ee942a8993701483045022100da6bee3c93766232079a01639d07fa869598749729ae323eab8eef53577d611b02207bef15429dcadce2121ea07f233115c6f09034c0be68db99980b9a6c5e75402201475221022626e955ea6ea6d98850c994f9107b036b1334f18ca8830bfff1295d21cfdb702103b287eaf122eea69030a0e9feed096bed8045c8b98bec453e1ffac7fbdbd4bb7152aeffffffff04d3b11400000000001976a914904a49878c0adfc3aa05de7afad2cc15f483a56a88ac7f400900000000001976a914418327e3f3dda4cf5b9089325a4b95abdfa0334088ac722c0c00000000001976a914ba35042cfe9fc66fd35ac2224eebdafd1028ad2788acdc4ace020000000017a91474d691da1574e6b3c192ecfb52cc8984ee7b6c568700000000' hex_sec = '03b287eaf122eea69030a0e9feed096bed8045c8b98bec453e1ffac7fbdbd4bb71' hex_der = '3045022100da6bee3c93766232079a01639d07fa869598749729ae323eab8eef53577d611b02207bef15429dcadce2121ea07f233115c6f09034c0be68db99980b9a6c5e754022' hex_redeem_script = '475221022626e955ea6ea6d98850c994f9107b036b1334f18ca8830bfff1295d21cfdb702103b287eaf122eea69030a0e9feed096bed8045c8b98bec453e1ffac7fbdbd4bb7152ae' sec = bytes.fromhex(hex_sec) der = bytes.fromhex(hex_der) # 해답 # sec 공개키와 der 서명을 통해 해당 객체 생성 point = S256Point.parse(sec) signature = Signature.parse(der) redeem_script = Script.parse(BytesIO(bytes.fromhex(hex_redeem_script))) stream = BytesIO(bytes.fromhex(hex_tx)) tx = Tx.parse(stream) # 두번째 서명을 만들기 위한 트랜잭션의 해시 계산 sig = int_to_little_endian(tx.version, 4) # 이전 트랜잭션 정보 : 이전 트랜잭션 개수 및 이전 트랜잭션들의 정보 sig += encode_varint(len(tx.tx_inputs)) # 서명 : 트랜잭션 입력의 해제 스크립트를 리딤 스크립트로 대체 cur_input = tx.tx_inputs[0] tx_in_copy = TxIn(prev_tx=cur_input.prev_tx, prev_index=cur_input.prev_index, script_sig=redeem_script, sequence=cur_input.sequence) sig += tx_in_copy.serialize() # 트랜잭션 출력 : 출력 개수 및 출력들의 정보 sig += encode_varint(len(tx.tx_outputs)) for tx_output in tx.tx_outputs: sig += tx_output.serialize() # 록타임 : 4바이트의 리틀엔디언 sig += int_to_little_endian(tx.locktime, 4) # 해시 유형 덧붙임 : SIGHASH_ALL 사용함 sig += int_to_little_endian(SIGHASH_ALL, 4) # 서명 해시 생성 result256 = hash256(sig) sig_message_hash = int.from_bytes(result256, "big") print(point.verify(sig_message_hash, signature))
def filerload(self, flag=1): """ 필터로드 메시지의 페이로드로 직렬화하여 메시지를 생성하는 함수 - 라이트 노드가 블룸 필터를 생성 후, 이 블룸 필터를 풀 노드에 전송해야함 - 풀 노드에게 보내는 VersionMessage의 렐레이 필드 값을 0으로 설정 - 풀 노드는 블룸 필터를 수신 받기 전까지 트랜잭션 메세지를 전송하지 않음 - 풀 노드로 전송하기 위한 블룸 필터 메시지 생성 필요 :param flag: 비트 필드 업데이트 플래그 - 0 : 풀 노드는 동작 중 비트 필드 업데이트 x - 1,2 : 특정 조건하에 새로운 입력을 필터에 더해 비트 필드 업데이트 :return: 직렬화한 페이로드를 담고 있는 GenericMessage 객체 """ # 비트 필드의 길이 정보 가변 정수 형식 payload = encode_varint(self.size) # 비트 필드 정보 payload += self.filter_bytes() # 해시 함수 개수 : 4바이트 리틀엔디언 payload += int_to_little_endian(self.function_count, 4) # tweak 정보 : 4바이트 리틀엔디언 payload += int_to_little_endian(self.tweak, 4) # matched item flag : 1 바이트 리틀엔디언 payload += int_to_little_endian(flag, 1) return GenericMessage(b"filterload", payload)
def verify_input(self, input_index): """ 현재 트랜잭션 입력에 대한 검증 step 1 : 현재 입력에 대응하는 결합 스크립트 생성 - 입력에 대응하는 이전 트랜잭션 출력의 잠금 스크립트 - 입력의 해제 스크립트 step 2 : 현재 입력에 대응하는 서명 생성 step 3 : 결합스크립트를 이용하여 서명 계산 :param input_index: 현재 입력에 대한 인덱스 :return: 검증 boolean """ # 해당 트랜잭션 해시값에 대응되는 트랜잭션의 잠금 스크립트 current_intput_tx = self.tx_inputs[input_index] current_pubkey = current_intput_tx.script_pubkey(testnet=self.testnet) # 스크립트 형식에 따라 리딤 스크립트 생성 if current_pubkey.is_p2sh_script_pubkey(): command = current_intput_tx.script_sig.commands[-1] redeem_script_raw = encode_varint(len(command)) + command redeem_script = Script.parse(BytesIO(redeem_script_raw)) else: redeem_script = None # 현재 트랜잭션 입력에 대한 결합 스크립트 combined = current_intput_tx.script_sig + current_pubkey # 현재 트랜잭션 입력에 대한 서명 생성 message_hash = self.sig_hash(input_index, redeem_script) # 결합 스크립트를 통한 서명 검증 return combined.evaluate(message_hash)
def serialize(self): # 프로토콜 버전 : 4바이트 리틀엔디언 ret = int_to_little_endian(self.version, 4) # 서비스 정보 : 8바이트 리틀엔디언 ret += int_to_little_endian(self.services, 8) # 타임스탬프 : 8바이트 리틀엔디언 ret += int_to_little_endian(self.timestamp, 8) # 수신자 서비스 정보 : 8바이트 리틀엔디언 ret += int_to_little_endian(self.receiver_services, 8) # 수신자 IP : 16바이트 (첫 12바이트는 IPv6 형식) ret += b'\x00' * 10 + b'\xff\xff' + self.receiver_ip # 수신자 포트 정보 : 2바이트 빅엔디언 ret += self.receiver_port.to_bytes(2, "big") # 송신자 서비스 정보 : 8바이트 리틀엔디언 ret += int_to_little_endian(self.sender_services, 8) # 송신자 IP : 16바이트 (첫 12바이트는 IPv6 형식) ret += b'\x00' * 10 + b'\xff\xff' + self.sender_ip # 송신자 포트 정보 : 2바이트 빅엔디언 ret += self.sender_port.to_bytes(2, "big") # 논스 : 8바이트 ret += self.nonce # 사용자 에이전트 : 가변필드 ret += encode_varint(len(self.user_agent)) ret += self.user_agent # 높이 : 4바이트 리틀엔디언 ret += int_to_little_endian(self.latest_block, 4) # 릴레이 : 2비트 if self.relay: ret += b"\x01" else: ret += b"\x00" return ret
def serialize(self): """ :return: 직렬화된 str """ # 버전 : 4바이트 리틀 엔디언 result = int_to_little_endian(self.version, 4) # 이전 트랜잭션 정보 : 이전 트랜잭션 개수 및 이전 트랜잭션들의 정보 result += encode_varint(len(self.tx_inputs)) for tx_input in self.tx_inputs: result += tx_input.serialize() # 트랜잭션 출력 : 출력 개수 및 출력들의 정보 result += encode_varint(len(self.tx_outputs)) for tx_output in self.tx_outputs: result += tx_output.serialize() # 록타임 : 4바이트의 리틀엔디언 result += int_to_little_endian(self.locktime, 4) return result
def serialize(self): """ :return: 길이 정보가 포함된 직렬화 """ # 명령어 집합에 대한 직렬화 result = self.raw_serialize() # 길이 정보 total = len(result) return encode_varint(total) + result
def serialize(self): # 요청하는 항목의 개수 : 가변 정수 형식 payload = encode_varint(len(self.data)) # 각 항목은 (유형, 항목을 특정하는 해시 식별자)로 구성 for data_type, identifier in self.data: # 유형 : 4바이트 리틀 엔디언 payload += int_to_little_endian(data_type, 4) # 해시 식별자 : 리틀 엔디언 payload += identifier[::-1] return payload
def serialize(self): # 프로토컬 버전 : 4바이트 리틀엔디언 ret = int_to_little_endian(self.version, 4) # 블록 헤더 개수 : 가변 정수 ret += encode_varint(self.num_hashes) # 시작 블록 : 리틀엔디언 ret += self.start_block[::-1] # 마지막 블록 : 리틀엔디언 ret += self.end_block[::-1] return ret
def serialize_segwit(self): """ 세그윗 트랜잭션 직렬화 - 이전 트랜잭션의 해제 스크립트는 증인 필드에 들어감 - 증인 필드 관련 정보는 마지막에 직렬화 :return: 직렬화된 str """ # 버전 : 4바이트 리틀 엔디언 result = int_to_little_endian(self.version, 4) # 마커 정보 result += b"\x00\x01" # 이전 트랜잭션 정보 : 이전 트랜잭션 개수 및 이전 트랜잭션들의 정보 result += encode_varint(len(self.tx_inputs)) for tx_input in self.tx_inputs: result += tx_input.serialize() # 트랜잭션 출력 : 출력 개수 및 출력들의 정보 result += encode_varint(len(self.tx_outputs)) for tx_output in self.tx_outputs: result += tx_output.serialize() # 각 입력에 있는 증인 필드 : 가변 정수 형식 for tx_in in self.tx_inputs: result += int_to_little_endian(len(tx_in.witness), 1) for item in tx_in.witness: if type(item) == int: result += int_to_little_endian(item, 1) else: result += encode_varint(len(item)) + item # 록타임 : 4바이트의 리틀엔디언 result += int_to_little_endian(self.locktime, 4) return result
def verify_input(self, input_index): """ 현재 트랜잭션 입력에 대한 검증 step 1 : 현재 입력에 대응하는 결합 스크립트 생성 - 입력에 대응하는 이전 트랜잭션 출력의 잠금 스크립트 - 입력의 해제 스크립트 - p2sh의 경우 리딤 스크립트 - 호환성을 위해 p2wpkh/p2wsh 또한 리딤 스크립트에 포함 - p2wpkh 스크립트의 경우, 증인필드(서명, 공개키) step 2 : 현재 입력에 대응하는 서명 생성 step 3 : 결합 스크립트를 이용하여 서명 계산 :param input_index: 현재 입력에 대한 인덱스 :return: 검증 boolean """ # 해당 트랜잭션 해시값에 대응되는 트랜잭션의 잠금 스크립트 current_intput_tx = self.tx_inputs[input_index] current_pubkey = current_intput_tx.script_pubkey(testnet=self.testnet) # 스크립트 형식에 따라 리딤 스크립트 생성 if current_pubkey.is_p2sh_script_pubkey(): command = current_intput_tx.script_sig.commands[-1] redeem_script_raw = encode_varint(len(command)) + command redeem_script = Script.parse(BytesIO(redeem_script_raw)) # 리딤 스크립트 : p2sh-p2wpkh 이거나 p2sh-p2wsh 이다 # p2wpkh인 경우 : if redeem_script.is_p2wpkh_script_pubkey(): message_hash = self.sig_hash_bip143(input_index, redeem_script) witness = current_intput_tx.witness # p2wsh인 경우 elif redeem_script.is_p2wsh_script_pubkey(): # 증인 스크립트 추출 cmd = current_intput_tx.witness[-1] # 가변 길이의 증인 스크립트 파싱 raw_witness = encode_varint(len(cmd)) + cmd witness_script = Script.parse(BytesIO(raw_witness)) message_hash = self.sig_hash_bip143( input_index, witness_script=witness_script) witness = current_intput_tx.witness # p2sh인 경우 else: message_hash = self.sig_hash(input_index, redeem_script) witness = None else: # 리딤 스크립트 : p2sh-p2wpkh 이거나 p2sh-p2wsh 이다 # p2wpkh인 경우 : if current_pubkey.is_p2wpkh_script_pubkey(): message_hash = self.sig_hash_bip143(input_index) witness = current_intput_tx.witness # p2wsh인 경우 : elif current_pubkey.is_p2wsh_script_pubkey(): # 증인 스크립트 추출 cmd = current_intput_tx.witness[-1] # 가변 길이의 증인 스크립트 파싱 raw_witness = encode_varint(len(cmd)) + cmd witness_script = Script.parse(BytesIO(raw_witness)) message_hash = self.sig_hash_bip143( input_index, witness_script=witness_script) witness = current_intput_tx.witness # p2sh인 경우 else: message_hash = self.sig_hash(input_index) witness = None # 현재 트랜잭션 입력에 대한 결합 스크립트 combined = current_intput_tx.script_sig + current_pubkey # 결합 스크립트를 통한 서명 검증 return combined.evaluate(message_hash, witness)
def evaluate(self, message_hash, witness): """ 결합 스크립트(잠금+해제)의 명령어들을 실행 - 실제에서는 해제 및 잠금 스크립트를 분리하여 실행 (why?) 해제 스크립트가 잠금 스크립트 실행에 영향을 주지 않기 위해 :param message_hash: 서명해시 :param witness: 세그윗인 경우 증인 필드(해제 스크립트가 포함되어 있음) :return: 실행 결과 boolean - True 인 경우만 해당 결합 스크립트가 유효하다는 의미 i.e 해제 스크립트로 잠금 스크립트를 해제 가능 p2sh 스크립트 - 리딤 스크립트 - 다중 서명 잠금 스크립트(공개키)에 해당 - 다중 서명 스크립트의 경우 길이가 매우 길다 => 다수의 공개키를 가지고 있어, 트랜잭션 출력의 잠금 스크립트 길이가 길어짐 - 리딤 스크립트의 해시를 잠금 스크립트에 포함 - [OP_HASH160, <hash>, OP_EQUAL] 형태 - 리딤 스크립트 자체는 해제 스크립트에 포함 - OP_HASH160(해제 스크립트의 리딤 스크립트) 와 <hash>가 같으면 유효하다고 판단 - 리딤 스크립트에 대해서 유효하다고 판단한 경우 - 리딤 스크립트의 명령어들(공개키포함)을 다시 명령어 집합(command)에 추가 - 이후 명령 스택과 함께 명령어들을 실행 - 명령어 집합이 [<리딤 스크립트>, OP_HASH160, <hash>, OP_EQUAL] 일 때가 예외 상황 => 해당 집합 실행 후 유효하다고 판단하면, 리딤 스크립트의 명령어들을 명령어집합에 추가 """ # 스크립트 명령집합에서 명령어가 실행되면서 명령어가 하나씩 삭제 # => commands 변수에 복사 commands = self.commands[:] stack = [] altstack = [] while len(commands) > 0: command = commands.pop(0) # 연산자 명령인 경우 : 해당 명령 실행 if type(command) == int: operation = OP_CODE_FUNCTIONS[command] # 흐름제어 명령어(99: op_if, 100: op_notif) if command in (99, 100): # 명령을 수행하지 못하는 경우 로거에 남기고 거짓 반환 if not operation(stack, commands): LOGGER.info("bad operation : {}".format( OP_CODE_NAMES[command])) print("bad operation : {}".format( OP_CODE_NAMES[command])) return False # 임시 스택(altstack)을 사용하는 명령어 # 107: op_toaltstack, 108: op_fromaltstack elif command in (107, 108): # 명령을 수행하지 못하는 경우 로거에 남기고 거짓 반환 if not operation(stack, altstack): LOGGER.info("bad operation : {}".format( OP_CODE_NAMES[command])) print("bad operation : {}".format( OP_CODE_NAMES[command])) return False # 서명해시(message_hash)가 필요한 명령어 # 172: op_checksig, 173: op_checksigverify # 174: op_checkmultisig, 175: op_checkmultisigverify, elif command in (172, 173, 174, 175): # 명령을 수행하지 못하는 경우 로거에 남기고 거짓 반환 if not operation(stack, message_hash): LOGGER.info("bad operation : {}".format( OP_CODE_NAMES[command])) print("bad operation : {}".format( OP_CODE_NAMES[command])) return False # 나머지 연산은 스택에 대한 연산들 else: if not operation(stack): LOGGER.info("bad operation : {}".format( OP_CODE_NAMES[command])) print("bad operation : {}".format( OP_CODE_NAMES[command])) return False # 연산자가 데이터인 경우 : 스택에 저장 else: stack.append(command) # p2sh 예외 상황 : [OP_HASH160, <hash>, OP_EQUAL] if len(commands) == 3 and \ commands[0] == 0xa9 and \ type(commands[1]) == bytes and len(commands[1]) == 20 and \ commands[2] == 0x87: # OP_HASH160는 필요 없음 commands.pop(0) # 리딤스크립트의 해시값 redeem_h160 = commands.pop(0) # OP_EQUAL도 필요없음 commands.pop(0) # 스택에 있는 리딤스크립트(해제 스크립트에 있던)를 해시 if not op_hash160(stack): return False # 스택에 리딤스크립트의 해시값 추가 stack.append(redeem_h160) # 잠금 스크립트에 있는 해시와 해시를 한 해제 스크립트 값 비교 if not op_equal(stack): return False if not op_verify(stack): LOGGER.info("bad p2sh hash160 ") print("bad p2sh hash160 ") return False # 스택에 넣었던 command가 리딤 스크립트 # 스크립트는 가변 길이 필드로 시작 => 스크립트 길이를 추가하여 파싱해야 한다 redeem_script = encode_varint(len(command)) + command stream = BytesIO(redeem_script) # 파싱한 명령어를 명령집합에 추가 commands.extend(Script.parse(stream).commands) # p2wpkh 예외 상황 : [0x00, <hash>] if len(stack) == 2 and stack[0] == b'' and len(stack[1]) == 20: # 0x00 은 필요없음 stack.pop(0) # 증인 필드 해시값 h160 = stack.pop(0) # 증인 필드(해제스크립트) 포함 commands.extend(witness) # 증인 필드 해시값을 통해 생성한 p2p2kh 스크립트의 명령어 추가 # 증인 필드와 증인필드 해시값을 비교하기 위한 작업 commands.extend(p2pkh_script(h160).commands) # p2wsh 예외 상황 : [0x00, <sha256>] - p2sh와 유사 if len(stack) == 2 and stack[0] == b'' and len(stack[1]) == 32: # 0x00 은 필요없음 stack.pop(0) # 증인 필드 해시값 s256 = stack.pop() commands.extend(witness[:-1]) # 증인 스크립트 : 증인 필드의 가장 마지막 항목 witness_script = witness[-1] # 증인 스크립트의 해시와 잠금 스크립트의 해시값 검증 if s256 != sha256(witness_script): print('bad sha256 {} vs {}'.format( s256.hex(), sha256(witness_script).hex())) return False # 증인 스크립트 팡싱 # 스크립트는 가변 길이 필드로 시작 => 스크립트 길이를 추가하여 파싱해야 한다 stream = BytesIO( encode_varint(len(witness_script)) + witness_script) witness_script_cmds = Script.parse(stream).commands commands.extend(witness_script_cmds) # 스택이 비어있는 경우, 스크립트 유효성 실패 if len(stack) == 0: print("stack is empty") return False # 스택의 최상위 원소가 비어있는 바이트인 경우에도 유효성 실패 # op.py 모듈의 연산 함수의 검증 실패시 스택에 비어있는 바이트를 스택에 올림 if stack.pop() == b"": print("stack has empty bytes") return False return True