Rusty Russell 작성
출처: https://rusty.ozlabs.org/2024/01/19/the-great-opcode-restoration.html
편집자 주: 이 글은 저자 Rusty Russell이 2024년 1월에 공개한 "위대한 스크립트 복원(Great Script Restoration)" 제안에 대한 개요입니다. "스크립트 복원"의 핵심 아이디어는 2010년 DoS 공격으로 인해 비활성화되었던 비트코인 스크립트 명령어(opcode)를 복원하는 것입니다. 동일한 DoS 공격으로 인해 이러한 명령어가 다시 활성화되는 것을 방지하기 위해 저자는 거래 검증 리소스 사용을 제한하는 새로운 방법과 새로운 명령어를 제안합니다.
2025년 9월 현재, 저자는 이 기사에서 다룬 아이디어 와 관련된 BIP를 4개 작성했습니다.
지난 몇 편의 게시물에서 저는 비트코인 스크립트(비트코인 스크립팅 언어)에 대한 내부 검토가 완료되면 어떤 업그레이드를 추가해야 할지 고민해 왔습니다. 스크립트는 서비스 거부 공격으로 인해 버전 0.3.1에서 중단되었습니다. 이는 항상 아쉬운 일이었지만, OP_TXHASH
와 같은 기능 덕분에 스크립트의 한계가 더욱 명확해졌습니다.
더 이상 사용되지 않는 비트코인 스크립트
많은 사람들 사토시 나카모토 버전 0.3.1에서 OP_CAT
및 기타 여러 명령어를 비활성화했다는 사실을 알고 있지만, 앤서니 타운스는 버전 0.3 이전에도 비트코인 소프트웨어가 OpenSSL의 BIGNUM 유형을 사용하여 임의 길이의 값을 구현하는 것을 허용했다고 지적했습니다.
당시는 비트코인 프로젝트 초창기였고, DoS 문제를 즉시 명확하게 피하고 신중하게 검토한 후에야 기능을 복구하고자 하는 바람을 충분히 이해합니다. 안타깝게도 사람들이 스크립트에 기능을 추가하는 것이 얼마나 어려운지 깨닫는 데는 몇 년이 걸렸습니다(지금도 마찬가지입니다).
가변 길이 명령어 예산: DoS를 도입하지 않고 스크립트 기능을 완전히 복원
BIP-342는 글로벌 서명 제한을 가중치 기반 서명 운영 예산 으로 대체하여 DoS를 방지할 수 있을 만큼 충분히 큰 규모로 합리적인 서명 검증(미니스크립트로 만들 수 있는 스크립트 등)을 지원하도록 설계되었습니다.
이 접근법은 다른 연산에도 적용할 수 있습니다. 단, 해당 연산의 비용이 피연산자의 크기에 비례해야 하며, 기존 스크립팅 시스템의 임의적인 제약을 제거하는 것도 가능합니다. 저는 이 아이디어를 "가변 길이 연산 코드(varops)" 예산 책정이라고 부르는데, 가변 길이 객체에 대한 연산에 적용되기 때문입니다.
내 초안 제안에서는 가변 길이 명령어 예산을 간단하게 설정합니다.
- 거래 가중치는 520으로 곱해집니다.
이렇게 하면 기존 스크립트에 예산이 적용되더라도 상상할 수 있는 모든 스크립트가 실행 불가능하지 않습니다(예: 모든 OP_SHA256은 항상 최대 길이의 스택 요소에서 작동할 수 있으며, 자체 명령어 가중치는 예산을 충당하기에 충분합니다).
참고: 이 예산은 입력당이 아닌 전체 거래에 적용됩니다. 이는 메모리 명령어를 예상한 것으로, 매우 짧은 스크립트가 매우 큰 입력을 확인할 수 있음을 의미합니다.
각 명령어에 의해 소모되는 예산은 다음과 같습니다(나열되지 않은 명령어는 예산을 소모하지 않습니다).
명령어 | 가변 길이 명령어 예산 소비 |
---|---|
OP_CAT | 0 |
OP_SUBSTR | 0 |
OP_LEFT | 0 |
OP_RIGHT | 0 |
OP_반전 | 1 + 길이(a) / 8 |
OP_AND | 1 + MAX(len(a), len(b)) / 8 |
OP_OR | 1 + MAX(len(a), len(b)) / 8 |
OP_XOR | 1 + MAX(len(a), len(b)) / 8 |
OP_2MUL | 1 + 길이(a) / 8 |
OP_2DIV | 1 + 길이(a) / 8 |
OP_ADD | 1 + MAX(len(a), len(b)) / 8 |
OP_SUB | 1 + MAX(len(a), len(b)) / 8 |
OP_MUL | (1 + len(a) / 8) * (1 + len(b) / 8 |
OP_DIV | (1 + len(a) / 8) * (1 + len(b) / 8 |
OP_MOD | (1 + len(a) / 8) * (1 + len(b) / 8 |
OP_LSHIFT | 1 + 길이(a) / 8 |
OP_RSHIFT | 1 + 길이(a) / 8 |
OP_EQUAL | 1 + MAX(len(a), len(b)) / 8 |
OP_NOTEQUAL | 1 + MAX(len(a), len(b)) / 8 |
OP_SHA256 | 1 + 길이(a) |
OP_RIPEMD160 | 0 (len(a) > 520바이트인 경우 실패) |
OP_SHA1 | 0 (len(a) > 520바이트인 경우 실패) |
OP_HASH160 | 1 + 길이(a) |
OP_HASH256 | 1 + 길이(a) |
다른 제한 사항을 제거하세요
Ethan Hilman의 OP_CAT 복원 제안은 520바이트 제한을 유지합니다. 가변 길이 opcode 예산을 사용하면 이 제한을 제거하고 taproot v1에 이미 적용된 총 스택 크기 제한(1,000개 요소 및 520,000바이트)으로 대체할 수 있습니다.
게다가 SegWit의 새로운 버전(예: Anthony Towns의 " 일반화된 탭루트 ")을 도입하거나 "키리스 엔트리 "를 허용하려는 경우 이러한 제한을 블록 크기에 대한 합리적인 상한(아마도 10,000개 요소 또는 최대 4M 바이트)에 적용할 수 있습니다.
의미론을 약간 변경합니다
값은 리틀 엔디안으로 유지되지만 부호 없는 값이 됩니다. 이를 통해 구현이 간소화되고 비트 연산과 산술 연산 간의 상호 작용이 훨씬 간단해집니다. 또한, 기존 양수에서 이러한 연산 코드를 수정이나 변환 없이 사용할 수 있습니다.
새로운 SegWit 버전을 사용하려는 경우 기존 명령어를 대체할 수 있습니다. 그렇지 않은 경우 새로운 명령어를 추가해야 합니다(예: OP_ADDV
).
구현 세부 사항
v0.3.0 비트코인 소프트웨어는 OpenSSL의 BIGNUM 유형을 중심으로 간단한 클래스 래퍼를 사용하지만, 최대한 단순하게 만들기 위해 외부 종속성 없이 모든 명령어를 다시 구현했습니다.
OP_EQUAL
/ OP_EQUALVERIFY
제외한 모든 명령어는 uint64_t
의 리틀 엔디안 벡터로(또는 리틀 엔디안 벡터에서) 변환됩니다. 이는 필요에 따라 변환하여 최적화할 수 있습니다.
OP_DIV
, OP_MOD
, OP_MUL
조잡하게 구현되었습니다(libgmp의 많은 수의 연산과 비교하면 보다 정교한 메서드가 훨씬 더 빠르다는 것을 알 수 있습니다).
벤치마킹: 위의 한도가 DoS를 방지하기에 충분히 낮습니까?
위의 한도가 무시할 만큼 충분히 높습니까?
520바이트 제한을 제거할 수 있습니다.
하지만 스택의 전체 크기에는 여전히 제한이 필요합니다. SegWit의 새 버전을 사용하면 이 제한을 400,000바이트로 늘릴 수도 있고, 현재 제한인 520,000바이트와 동일하게 유지할 수도 있습니다.
이전 기사 " 스크립트에서 스크립트 공개 키 결정 "( 중국어 번역 )에서 저는 때때로 정확한 스크립트가 아닌 특정 유형의 스크립트 조건이 필요하다는 점을 지적했습니다. 예를 들어 안전 계약 유형의 제약 조항은 지연을 요구하지만 스크립트에 다른 내용이 있는지는 신경 쓰지 않습니다.
문제는 Taproot 스크립트에서 알 수 없는 명령어( OP_SUCCESSx
)가 있으면 스크립트 전체가 실패(전혀 실행되지 않음)하게 되므로, 이 부분을 약간 수정해야 한다는 것입니다. 이전에 제안했던 구분 기호는 다소 어색했기 때문에, 더 간단한 새 해결책을 생각해 냈습니다.
OP_SEGMENT 추가
현재 검증기는 탭스크립트 전체에서 OP_SUCCESS
명령어를 검색하여 찾으면 스크립트가 통과합니다. 이는 다음과 같이 변경됩니다.
- 탭스크립트에서
OP_SEGMENT
와OP_SUCCESSx
스캔합니다. -
OP_SEGMENT
발견되면 이 명령어 앞에 있는 스크립트가 실행됩니다. 스크립트가 실패하지 않으면 이 명령어부터 스캐닝이 계속됩니다. -
OP_SUCCESSx
발견되면 스크립트는 직접 전달됩니다.
기본적으로 이 기능은 스크립트를 여러 세그먼트 로 분할하여 각 세그먼트가 순차적으로 실행되도록 합니다. "OP_SEGMENT를 사용하여 스크립트를 세그먼트로 분할하고 한 번에 한 세그먼트만 실행"하는 것만큼 간단하지는 않습니다. 탭스크립트에서는 OP_SUCCESSx
뒤에 디코딩할 수 없는 콘텐츠를 포함할 수 있기 때문에, 이 기능을 유지하고 싶습니다.
OP_SEGMENT
실행할 때는 아무 일도 일어나지 않습니다. OP_SUCCESS
명령어의 적용 범위를 제한하기 위해서만 존재합니다.
성취하다
ExecuteWitnessScript
리팩토링이 필요할 것입니다(38줄의 코드 중 21줄이 "if Tapscript" 조건문이므로 전용 ExecuteTapScript
로 변경해야 할 수도 있음). 또한 OP_SUCCESS
가 뒤따르더라도 OP_SEGMENT
발생하면 현재 tapscript의 스택 제한이 적용된다는 것을 의미합니다.
흥미로운 점은 OP_SEGMENT
무시하는 것 외에는 EvalScript
핵심 기능이 변경되지 않는다는 것입니다. 이미 매우 유연하기 때문입니다.
경고하자면, 아직 구현을 완료하지 않았으므로 놀랄 일이 있을 수 있지만, 아이디어에 대한 리뷰가 나오면 프로토타입을 만들 계획입니다.
마음에 드셨으면 좋겠습니다!
(위에)