티스토리 뷰
본 게시글에서는 저서 '밑바닥부터 시작하는 비트코인'의 Python으로 작성된 예제 코드를 Go로 컨버팅 하여 작성하였습니다.
📺시리즈
2023.08.25 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 1장 유한체
2023.08.27 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 2장 타원곡선
2023.08.30 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 3장 타원곡선 암호
2023.09.02 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 4장 직렬화
2023.09.05 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 5장 트랜잭션
2023.09.11 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 6장 스크립트
2023.09.16 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 7장 트랜잭션 검증과 생성
2023.09.20 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 8장 p2sh 스크립트
2023.09.21 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 9장 블록
2024.01.08 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 10장 네트워킹
2024.01.09 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 11장 단순 지급 검증
2024.01.10 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 12장 블룸 필터
2024.01.13 - [블록체인/Bitcoin] - 밑바닥부터 시작하는 비트코인 - 13장 세그윗 1
🐲 전체 코드
🥅 학습 목표
1. p2wsh 스크립트와 p2sh- p2wsh 스크립트에 대해 알아봅니다.
2. BIP0143 세그윗 서명해시에 대해 알아봅니다.
📜 1. p2wsh 스크립트
p2wsh(pay-to-witness-script-hash) 스크립트는 다중 서명과 같은 복잡한 기능을 유연하게 처리하게 위해 고안된 방식입니다. p2wsh와 p2sh의 유일한 차이점은 해제 스크립트의 모든 데이터가 증인 필드에 위치한다는 것입니다.
1.1 p2wsh 스크립트 실행 순서
잠금 스크립트와 해제 스크립트를 결합하여 명령집합을 구성합니다. 이때 해제 스크립트는 p2wpkh 스크립트와 마찬가지로 비어있습니다.
스크립트를 실행하고 나면 스택에는 0 <32-byte hash>가 남게 되고 세그윗 이전 노드는 더 이상 실행할 명령어가 없으므로 실행을 종료합니다. 반면에 세그윗 노드는 OP_0 <32-byte hash> 패턴이 매칭되면 특별 규칙을 실행합니다. 세그윗 노드는 증인 필드의 마지막 항목, 증인 스크립트(WitnessScript)를 가져옵니다. 그리고 증인 스크립트의 sha256 해시값을 스택에 들어있는 <32-byte hash>와 비교하여 동일하다면 증인 스크립트를 제외한 증인 필드의 모든 값을 명령집합에 추가합니다. 그리고 증인 스크립트를 파싱하여 명령집합에 추가합니다. 이 모든 과정을 거친 최종적인 명령집합은 다음과 같습니다.
이는 p2sh 스크립트와 유사한 2-of-3 다중서명입니다. 만약 서명이 유효하다면 1, 그렇지 않은 경우 0을 스택의 맨 위에 추가하고 스크립트 실행은 종료됩니다.
📜 2. p2sh-p2wsh 스크립트
p2sh-p2wsh 스크립트는 p2sh-p2wpkh처럼 세그윗 이전에 생성된 지갑들로 p2wsh 트랜잭션을 생성할 수 있도록 고안한 방식입니다.
2.1 p2sh-p2wsh 스크립트 실행 순서
p2sh-p2wsh 스크립트의 잠금 스크립트는 여느 p2sh의 것과 같은 형태이고, 해제 스크립트는 리딤 스크립트입니다.
스크립트 실행도 동일합니다. BIP0016을 미지원하는 노드는 스크립트가 유효하다고 판단하고 종료하고 지원하는 노드는 p2sh 특별 규칙을 실행하여 리딤 스크립트를 파싱하여 명령집합에 추가합니다. 리딤 스크립트는 p2wsh 잠금 스크립트와 동일한 OP_0 <32-byte hash>입니다.
나머지는 p2wsh 스크립트와 동일합니다. 세그윗을 지원하지 않는 노드는 스택에 0 <32-byte hash>가 남으면 스크립트가 유효하다고 판단하고 종료, 지원하는 노드는 증인 필드와 증인 스크립트를 명령집합에 추가하여 추가로 검증을 이어나갑니다. 이런 식으로 p2sh 잠금 스크립트를 지원하는 세그윗 이전 지갑으로 p2wsh 형식의 UTXO를 소비할 수 있습니다.
🖊️ 3. 세그윗 서명해시 및 그 외 개선 사항
3.1 세그윗 서명해시
세그윗은 세그윗 이전 트랜잭션과 서명해시를 다르게 계산하여 이차 해싱 문제를 해결합니다. 각 입력의 서명을 생성할 때마다 서명해시를 새로 생성해야 했던 것과 비교해 많은 부분을 재사용할 수 있게 되었고 서명해시를 계산하는 입력값의 길이가 120바이트로 고정됨으로 서명해시의 계산 시간이 입력의 개수의 제곱 시간에서 선형 시간으로 개선되었습니다.
세그윗 해시는 또한 오프라인 지갑 수수료 계산 및 보안을 강화했습니다. 세그윗 이전에는 트랜잭션의 서명을 생성하는 과정에서 입력이 사용하는 비트코인의 양이 포함되지 않았습니다. 네트워크와 아주 적은 데이터만 주고받는 오프라인 지갑(콜드 월렛)의 경우 입력 금액을 알 수 없기 때문에 정확한 트랜잭션 수수료를 측정하는 데 어려움이 있었고 공격자로부터 잘못된 금액을 전달받아 트랜잭션에 서명하게 될 우려가 있었습니다. 세그윗 서명해시는 입력 금액을 서명해시에 포함함으로써 오프라인 지갑에서 트랜잭션 수수료를 계산할 수 있게 하고 잘못된 금액을 전달받아 트랜잭션에 서명했더라도 네트워크에서 서명과 트랜잭션을 무효화할 수 있게 하였습니다.
👇 세그윗 서명해시를 계산하는 코드
// BIP143에 따라 트랜잭션의 서명해시를 반환하는 함수
func (t *Tx) SigHashBIP143(inputIndex int, redeemScript *script.Script, witnessScript *script.Script) ([]byte, error) {
if inputIndex >= len(t.Inputs) {
return nil, fmt.Errorf("input index %d greater than the number of inputs %d", inputIndex, len(t.Inputs))
}
txin := t.Inputs[inputIndex] // 입력을 가져옴
buf := new(bytes.Buffer)
_, err := buf.Write(utils.IntToLittleEndian(t.Version, 4)) // 버전 (4바이트, 리틀엔디언)
if err != nil {
return nil, err
}
hashPrevOuts, err := t.hashPrevouts()
if err != nil {
return nil, err
}
_, err = buf.Write(hashPrevOuts) // 이전 트랜잭션 출력의 해시 (32바이트)
if err != nil {
return nil, err
}
hashSequence, err := t.hashSequence()
if err != nil {
return nil, err
}
_, err = buf.Write(hashSequence) // 시퀀스 번호의 해시 (32바이트)
if err != nil {
return nil, err
}
hexPrevTx, err := hex.DecodeString(txin.PrevTx) // 문자열을 16진수로 디코딩 (이 부분이 빠져있었음)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.ReverseBytes(hexPrevTx)) // 이전 트랜잭션의 해시 (32바이트, 리틀엔디언)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.IntToLittleEndian(txin.PrevIndex, 4)) // 이전 트랜잭션의 출력 인덱스 (4바이트, 리틀엔디언)
if err != nil {
return nil, err
}
var scriptCode []byte
if witnessScript != nil {
scriptCode, err = witnessScript.Serialize()
if err != nil {
return nil, err
}
} else if redeemScript != nil {
scriptCode, err = script.NewP2pkhScript(redeemScript.Cmds[1].Elem).Serialize()
if err != nil {
return nil, err
}
} else {
scriptPubkey, err := txin.ScriptPubKey(NewTxFetcher(), t.Testnet)
if err != nil {
return nil, err
}
scriptCode, err = script.NewP2pkhScript(scriptPubkey.Cmds[1].Elem).Serialize()
if err != nil {
return nil, err
}
}
_, err = buf.Write(scriptCode) // 스크립트 코드
if err != nil {
return nil, err
}
val, err := txin.Value(NewTxFetcher(), t.Testnet)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.IntToLittleEndian(val, 8)) // 이전 트랜잭션 출력의 금액 (8바이트, 리틀엔디언)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.IntToLittleEndian(txin.SeqNo, 4)) // 시퀀스 번호 (4바이트, 리틀엔디언)
if err != nil {
return nil, err
}
hashOutputs, err := t.hashOutputs()
if err != nil {
return nil, err
}
_, err = buf.Write(hashOutputs) // 출력의 해시 (32바이트)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.IntToLittleEndian(t.Locktime, 4)) // 유효 시점 (4바이트, 리틀엔디언)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.IntToLittleEndian(SIGHASH_ALL, 4)) // SIGHASH_ALL (4바이트, 리틀엔디언)
if err != nil {
return nil, err
}
h256 := utils.Hash256(buf.Bytes()) // 해시를 생성
return h256, nil // 해시를 반환
}
3.2 그 외 개선사항
비압축 SEC 공개키 사용을 정책으로 금지하고 압축 SEC 공개키만 사용할 수 있게 함으로써 저장 공간을 절약하였습니다.
🖥️ 4. 코드 작성 및 수정
4.1 기존 코드 수정사항
우선은 수정사항이 많습니다. 세그윗이 적용되면서 수정된 부분도 있고, 세그윗 관련 코드가 너무 안돌아가서 예제에 있는 테스트 코드를 싹 돌려보면서 발견한 문제점들도 있습니다. 솔직히 너무 치명적인데 지금까지 무지성으로 넘겨버린 탓에 발견하지 못한 점, 깊이 반성하고 있습니다...
4.1.1 스크립트 명령어 타입 추가
스크립트를 평가할 때 any 타입의 슬라이스에서 값을 가져와 타입을 일일히 검사하는 부분이 번거롭고 지저분한 것 같아서 추가를 해 보았습니다.
4.1.2 슬라이스를 뒤집을 때 복사본을 만들어 뒤집기
기존 원본 슬라이스의 원소들의 순서를 변경하는 경우는 값을 재사용하려면 다시 뒤집어야 하므로 새로운 슬라이스를 만들어 반환하도록 변경했습니다. 이때 원소의 개수가 홀수 개인 경우 가운데에 있는 원소의 복사가 이루어지지 않을 수 있으므로 탐색 범위에 n/2를 포함합니다.
4.1.3 블록의 해시가 아니라 블록을 직렬화한 값의 hash256 해시값
작업증명의 유효성 검증은 블록 해시를 구하기 위해 바이트 슬라이스를 뒤집어주기 이전의 값을 사용해야 하는데 이 부분을 간과하고 해시값을 사용해서 검증 단계에서 문제가 있었습니다.
4.2 세그윗 적용
4.2.1 트랜잭션에 세그윗 관련 필드 추가
세그윗인지 여부와 이전 트랜잭션 출력의 해시, 입력 시퀀스의 해시 그리고 출력의 해시를 추가하였습니다. 세그윗 여부를 제외한 3개는 세그윗 서명해시를 구하는 과정에서 필요한 값들입니다.
👇 이전 트랜잭션 출력의 해시, 입력 시퀀스의 해시 그리고 출력의 해시를 구하는 코드
func (t *Tx) hashPrevouts() ([]byte, error) {
if t.HashPrevOuts != nil {
return t.HashPrevOuts, nil
}
hashBuf := new(bytes.Buffer)
seqBuf := new(bytes.Buffer)
for _, input := range t.Inputs {
hexPrevTx, err := hex.DecodeString(input.PrevTx) // 문자열을 16진수로 디코딩
if err != nil {
return nil, err
}
_, err = hashBuf.Write(utils.ReverseBytes(hexPrevTx))
if err != nil {
return nil, err
}
_, err = hashBuf.Write(utils.IntToLittleEndian(input.PrevIndex, 4))
if err != nil {
return nil, err
}
_, err = seqBuf.Write(utils.IntToLittleEndian(input.SeqNo, 4))
if err != nil {
return nil, err
}
}
t.HashPrevOuts = utils.Hash256(hashBuf.Bytes())
t.HashSequence = utils.Hash256(seqBuf.Bytes())
return t.HashPrevOuts, nil
}
func (t *Tx) hashSequence() ([]byte, error) {
if t.HashSequence != nil {
return t.HashSequence, nil
}
_, err := t.hashPrevouts()
if err != nil {
return nil, err
}
return t.HashSequence, nil
}
func (t *Tx) hashOutputs() ([]byte, error) {
if t.HashOutputs != nil {
return t.HashOutputs, nil
}
buf := new(bytes.Buffer)
for _, output := range t.Outputs {
b, err := output.Serialize()
if err != nil {
return nil, err
}
_, err = buf.Write(b)
if err != nil {
return nil, err
}
}
t.HashOutputs = utils.Hash256(buf.Bytes())
return t.HashOutputs, nil
}
4.2.2 TxFetcher의 Fetch 메서드
세그윗을 적용하기 이전에 세그윗 마커와 플래그를 제거하고 트랜잭션을 파싱했는데 더 이상 그럴 필요가 없어졌기 때문에 수정이 필요합니다. 이 부분은 교재에서도 언급을 안 해줘서 찾는 데 어려움이 있었습니다.
4.2.3 세그윗 트랜잭션 직렬화
트랜잭션이 세그윗인지 여부에 따라 다른 직렬화 메서드를 사용해 트랜잭션을 직렬화합니다. 기존의 직렬화 로직은 serializeLegacy 메서드에서 그대로 사용하고 세그윗 트랜잭션 직렬화에는 세그윗 마커, 플래그 그리고 증인 데이터가 추가됩니다.
👇 세그윗 트랜잭션 직렬화 코드
// 트랜잭션을 직렬화한 결과를 반환하는 함수
func (t Tx) Serialize() ([]byte, error) {
if t.Segwit { // 세그윗 트랜잭션인 경우
return t.serializeSegwit()
}
return t.serializeLegacy()
}
func (t Tx) serializeSegwit() ([]byte, error) {
buf := new(bytes.Buffer)
_, err := buf.Write(utils.IntToLittleEndian(t.Version, 4)) // 버전 (4바이트, 리틀엔디언)
if err != nil {
return nil, err
}
_, err = buf.Write([]byte{0x00, 0x01}) // 마커 (0x00), 플래그 (0x01)
if err != nil {
return nil, err
}
_, err = buf.Write(utils.EncodeVarint(len(t.Inputs))) // 입력 개수 (가변 정수)
if err != nil {
return nil, err
}
for _, input := range t.Inputs {
s, err := input.Serialize()
if err != nil {
return nil, err
}
_, err = buf.Write(s) // 입력
if err != nil {
return nil, err
}
}
_, err = buf.Write(utils.EncodeVarint(len(t.Outputs))) // 출력 개수
if err != nil {
return nil, err
}
for _, output := range t.Outputs {
s, err := output.Serialize()
if err != nil {
return nil, err
}
_, err = buf.Write(s) // 출력
if err != nil {
return nil, err
}
}
witnesses, err := t.serializeWitnesses() // 증인 데이터를 직렬화
if err != nil {
return nil, err
}
_, err = buf.Write(witnesses) // 증인 데이터
if err != nil {
return nil, err
}
_, err = buf.Write(utils.IntToLittleEndian(t.Locktime, 4)) // 유효 시점
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// 증인 데이터를 직렬화한 결과를 반환하는 함수
func (t Tx) serializeWitnesses() ([]byte, error) {
buf := new(bytes.Buffer)
for _, input := range t.Inputs {
_, err := buf.Write(utils.IntToLittleEndian(len(input.Witness), 1)) // 입력의 증인 데이터 개수
if err != nil {
return nil, err
}
for _, item := range input.Witness {
if len(item) == 0 {
_, err := buf.Write(utils.IntToLittleEndian(0, 1)) // 증인 데이터의 길이가 0이면 0x00을 추가
if err != nil {
return nil, err
}
continue
}
_, err := buf.Write(utils.EncodeVarint(len(item))) // 증인 데이터의 길이 (가변 정수)
if err != nil {
return nil, err
}
_, err = buf.Write(item) // 증인 데이터
if err != nil {
return nil, err
}
}
}
return buf.Bytes(), nil // 직렬화한 결과를 반환
}
4.2.4 세그윗 트랜잭션 입력 검증
👇 세그윗 트랜잭션 입력 검증 코드
// 트랜잭션의 입력을 검증하는 함수
func (t Tx) VerifyInput(inputIndex int) (bool, error) {
if inputIndex >= len(t.Inputs) {
return false, fmt.Errorf("input index %d greater than the number of inputs %d", inputIndex, len(t.Inputs))
}
input := t.Inputs[inputIndex] // 입력을 가져옴
scriptSig := input.ScriptSig // 해제 스크립트
scriptPubKey, err := input.ScriptPubKey(NewTxFetcher(), t.Testnet) // 이전 트랜잭션 출력의 잠금 스크립트를 가져옴
if err != nil {
return false, err
}
var z []byte
var witness [][]byte
if script.IsP2shScriptPubkey(scriptPubKey.Cmds) { // 이전 트랜잭션 출력의 잠금 스크립트가 p2sh 스크립트인 경우
command := scriptSig.Cmds[len(scriptSig.Cmds)-1] // 해제 스크립트의 마지막 명령어
if command.IsOpCode { // 마지막 명령어가 op 코드인 경우 에러를 반환
return false, fmt.Errorf("last command should be redeem script")
}
rawRedeem := append(utils.IntToLittleEndian(len(command.Elem), 1), command.Elem...) // 리딤 스크립트를 가져옴
redeemScript, _, err := script.Parse(rawRedeem) // 리딤 스크립트를 파싱
if err != nil {
return false, err
}
if script.IsP2wpkhScriptPubkey(redeemScript.Cmds) { // 리딤 스크립트가 p2wpkh 스크립트인 경우
z, err = t.SigHashBIP143(inputIndex, redeemScript, nil) // BIP143에 따라 서명해시를 생성
if err != nil {
return false, err
}
witness = input.Witness
} else if script.IsP2wshScriptPubkey(redeemScript.Cmds) { // 리딤 스크립트가 p2wsh 스크립트인 경우
cmd := input.Witness[len(input.Witness)-1] // 증인 데이터의 마지막 명령어
rawWitness := append(utils.IntToLittleEndian(len(cmd), 1), cmd...) // 증인 데이터를 가져옴
witnessScript, _, err := script.Parse(rawWitness) // 증인 데이터를 파싱
if err != nil {
return false, err
}
z, err = t.SigHashBIP143(inputIndex, nil, witnessScript) // BIP143에 따라 서명해시를 생성
if err != nil {
return false, err
}
witness = input.Witness
} else {
z, err = t.SigHash(inputIndex, redeemScript) // 서명해시를 생성
if err != nil {
return false, err
}
witness = nil
}
} else {
if script.IsP2wpkhScriptPubkey(scriptPubKey.Cmds) {
z, err = t.SigHashBIP143(inputIndex, nil, nil)
if err != nil {
return false, err
}
witness = input.Witness
} else if script.IsP2wshScriptPubkey(scriptPubKey.Cmds) {
cmd := input.Witness[len(input.Witness)-1]
rawWitness := append(utils.IntToLittleEndian(len(cmd), 1), cmd...)
witnessScript, _, err := script.Parse(rawWitness)
if err != nil {
return false, err
}
z, err = t.SigHashBIP143(inputIndex, nil, witnessScript)
if err != nil {
return false, err
}
witness = input.Witness
} else {
z, err = t.SigHash(inputIndex)
if err != nil {
return false, err
}
witness = nil
}
}
combined := scriptSig.Add(scriptPubKey) // 해제 스크립트와 잠금 스크립트를 결합
return combined.Evaluate(z, witness) // 결합한 스크립트를 평가
}
4.2.5 세그윗 스크립트 평가
👇 세그윗 스크립트 평가
// 스크립트 명령어 집합을 순회하면서 스크립트가 유효한지 확인
func (s Script) Evaluate(z []byte, witness [][]byte) (bool, error) {
cmds := s.Cmds
stack := []any{} // 스택
altstack := []any{} // 대체 스택
// 스크립트 명령어를 순회하면서 스택에 데이터를 추가하거나 연산을 수행
for len(cmds) > 0 {
cmd := cmds[0] // 스크립트 명령어 집합의 첫 번째 원소
cmds = cmds[1:] // 스크립트 명령어 집합의 첫 번째 원소 제거
// 스크립트 명령어가 연산자에 해당하는 경우
if cmd.IsOpCode {
operation := OpCodeFuncs[cmd.Code] // 연산자에 해당하는 함수 가져오기
switch {
case cmd.Code >= 99 && cmd.Code <= 100:
fn, ok := operation.(func(*[]any, *[]Command) bool) // 연산자에 해당하는 함수가 func(*[]any, *[]Command) bool 타입인지 확인
if !ok {
return false, fmt.Errorf("failed to cast evaluate func: %s", cmd.Code.String())
}
if !fn(&stack, &cmds) { // stack과 cmds를 인자로 연산자에 해당하는 함수 호출
return false, fmt.Errorf("failed to evaluate %s", cmd.Code.String())
}
case cmd.Code >= 107 && cmd.Code <= 108:
fn, ok := operation.(func(*[]any, *[]any) bool) // 연산자에 해당하는 함수가 func(*[]any, *[]any) bool 타입인지 확인
if !ok {
return false, fmt.Errorf("failed to cast evaluate func: %s", cmd.Code.String())
}
if !fn(&stack, &altstack) { // stack과 altstack을 인자로 연산자에 해당하는 함수 호출
return false, fmt.Errorf("failed to evaluate %s", cmd.Code.String())
}
case cmd.Code >= 172 && cmd.Code <= 175:
fn, ok := operation.(func(*[]any, []byte) bool) // 연산자에 해당하는 함수가 func(*[]any, []byte) bool 타입인지 확인
if !ok {
return false, fmt.Errorf("failed to cast evaluate func: %s", cmd.Code.String())
}
if !fn(&stack, z) { // stack과 z를 인자로 연산자에 해당하는 함수 호출
return false, fmt.Errorf("failed to evaluate %s", cmd.Code.String())
}
default:
fn, ok := operation.(func(*[]any) bool) // 연산자에 해당하는 함수가 func(*[]any) bool 타입인지 확인
if !ok {
return false, fmt.Errorf("failed to cast evaluate func: %s", cmd.Code.String())
}
if !fn(&stack) { // stack을 인자로 연산자에 해당하는 함수 호출
return false, fmt.Errorf("failed to evaluate %s", cmd.Code.String())
}
}
continue
}
// 스크립트 명령어가 []byte 타입인 경우: 스택에 추가
stack = append(stack, cmd.Elem)
// p2sh 스크립트인 경우: 스크립트 명령어 집합의 첫 번째 원소가 OP_HASH160, 두 번째 원소가 20바이트의 데이터, 세 번째 원소가 OP_EQUAL인지 확인
if len(cmds) == 3 && cmds[0].Code == OpCodeHash160 && len(cmds[1].Elem) == 20 && cmds[2].Code == OpCodeEqual {
// 스크립트 명령어 집합의 세 개의 원소를 제거 (OP_HASH160, 20바이트의 데이터, OP_EQUAL)
cmds = cmds[1:]
h160 := cmds[0].Elem
cmds = cmds[2:]
if !OpHash160(&stack) { // OP_HASH160 연산자 수행
return false, errors.New("failed to evaluate OP_HASH160")
}
stack = append(stack, h160) // 스택에 20바이트의 데이터 추가
if !OpEqual(&stack) { // OP_EQUAL 연산자 수행
return false, errors.New("failed to evaluate OP_EQUAL")
}
if !OpVerify(&stack) { // OP_VERIFY 연산자 수행
return false, errors.New("failed to evaluate OP_VERIFY")
}
// 스크립트 명령어 집합에 리딤 스크립트를 추가
rawRedeem := append(utils.EncodeVarint(len(cmd.Elem)), cmd.Elem...)
redeemScript, _, err := Parse(rawRedeem)
if err != nil {
return false, err
}
cmds = append(cmds, redeemScript.Cmds...)
}
// p2wpkh 스크립트인 경우: 스택의 원소가 0과 20바이트의 데이터인지 확인
if len(stack) == 2 && len(stack[0].([]byte)) == 0 && len(stack[1].([]byte)) == 20 {
h160 := stack[1].([]byte) // 스택의 두 번째 원소를 20바이트의 데이터로 변환
// 스택의 원소를 모두 제거
stack = []any{}
// 명령어 집합에 witness를 추가
for _, item := range witness {
cmds = append(cmds, NewElem(item))
}
// 명령어 집합에 p2pkh 스크립트를 추가
cmds = append(cmds, NewP2pkhScript(h160).Cmds...)
}
// p2wsh 스크립트인 경우: 스택의 원소가 0과 32바이트의 데이터인지 확인
if len(stack) == 2 && len(stack[0].([]byte)) == 0 && len(stack[1].([]byte)) == 32 {
h256 := stack[1].([]byte) // 스택의 가장 마지막 원소를 32바이트의 데이터로 변환
// 스택의 원소를 모두 제거
stack = []any{}
// 증인 스크립트를 제외한 나머지 증인 데이터를 스택에 추가
for i := 0; i < len(witness)-1; i++ {
stack = append(stack, witness[i])
}
rawWitnessScript := witness[len(witness)-1] // witness의 마지막 원소를 증인 스크립트로 변환
if !bytes.Equal(utils.Sha256(rawWitnessScript), h256) { // 증인 스크립트의 SHA256 해시값과 스택의 마지막 원소가 같은지 확인
return false, errors.New("invalid witness script")
}
buf := new(bytes.Buffer) // 버퍼 생성
_, err := buf.Write(utils.EncodeVarint(len(rawWitnessScript))) // 증인 스크립트의 길이를 가변 정수로 직렬화하여 버퍼에 추가
if err != nil {
return false, err
}
_, err = buf.Write(rawWitnessScript) // 증인 스크립트를 버퍼에 추가
if err != nil {
return false, err
}
witnessScript, _, err := Parse(buf.Bytes()) // 버퍼의 데이터를 스크립트로 변환
if err != nil {
return false, err
}
cmds = append(cmds, witnessScript.Cmds...) // 명령어 집합에 witness 스크립트를 추가
}
}
// 스택이 비어있는 경우: 스크립트가 유효하지 않음
if len(stack) == 0 {
return false, nil
}
switch last := stack[len(stack)-1].(type) {
// 스택의 마지막 원소가 int 타입인 경우: 해당 원소가 0이 아닌 경우 스크립트가 유효함
case int:
if last == 0 {
return false, nil
}
// 스택의 마지막 원소가 []byte 타입인 경우: 해당 원소가 비어있지 않은 경우 스크립트가 유효함
case []byte:
if len(last) == 0 {
return false, nil
}
// 그 외의 경우: 에러 반환
default:
return false, errors.New("invalid stack element")
}
return true, nil
}
📖 참고자료
'블록체인 > Bitcoin' 카테고리의 다른 글
밑바닥부터 시작하는 비트코인 - 외전2 비트코인 백서 의식의 흐름대로 읽기 (0) | 2024.01.19 |
---|---|
밑바닥부터 시작하는 비트코인 - 외전1 네트워크 요청 및 응답 개선 (0) | 2024.01.17 |
밑바닥부터 시작하는 비트코인 - 13장 세그윗 1 (0) | 2024.01.13 |
밑바닥부터 시작하는 비트코인 - 12장 블룸 필터 (1) | 2024.01.10 |
밑바닥부터 시작하는 비트코인 - 11장 단순 지급 검증 (1) | 2024.01.09 |