이미지 로딩 중...
AI Generated
2025. 11. 10. · 2 Views
타입스크립트로 비트코인 클론하기 11편 - 채굴 난이도 조절 메커니즘
비트코인의 핵심 메커니즘인 난이도 조절 시스템을 타입스크립트로 구현합니다. 블록 생성 시간을 일정하게 유지하는 방법과 실제 작동 원리를 코드로 배워보세요.
목차
- 난이도 조절의 필요성 - 블록체인의 안정성을 지키는 핵심 메커니즘
- 실제 소요 시간 계산 - 난이도 조절의 기준 데이터
- 새 난이도 계산 - 비율 기반 자동 조정
- 난이도와 해시 비교 - 채굴 성공 판정의 핵심
- 블록 생성 시 난이도 결정 - 체인에 새 블록 추가하기
- 채굴 루프와 난이도 - nonce 찾기 과정
- 블록체인 검증과 난이도 - 전체 체인의 유효성 확인
- 난이도 폭발과 Ice Age - 비트코인과 이더리움의 차이점
1. 난이도 조절의 필요성 - 블록체인의 안정성을 지키는 핵심 메커니즘
시작하며
여러분이 블록체인을 운영하는데 갑자기 채굴자가 10배로 늘어났다고 상상해보세요. 그러면 블록이 10분마다 생성되던 것이 1분마다 생성될 것입니다.
반대로 채굴자가 절반으로 줄면 20분마다 블록이 생성되겠죠. 이런 문제는 실제 블록체인 네트워크에서 심각한 문제를 일으킵니다.
블록 생성 속도가 불안정하면 거래 처리 시간을 예측할 수 없고, 네트워크의 보안성도 떨어지게 됩니다. 사용자들은 "내 거래가 언제 처리될지" 알 수 없어 혼란스러워합니다.
바로 이럴 때 필요한 것이 난이도 조절 메커니즘입니다. 비트코인은 이 메커니즘을 통해 10년 넘게 평균 10분마다 블록을 생성하는 놀라운 안정성을 유지하고 있습니다.
개요
간단히 말해서, 난이도 조절은 블록 생성 속도를 일정하게 유지하기 위해 채굴 난이도를 자동으로 조정하는 메커니즘입니다. 비트코인에서는 2016개의 블록마다 난이도를 재조정합니다.
왜 2016개일까요? 10분마다 블록이 생성되면 2016개는 정확히 2주(14일)에 해당합니다.
이 주기는 네트워크의 해시파워 변화에 충분히 빠르게 반응하면서도, 일시적인 변동에는 영향받지 않을 만큼 안정적입니다. 기존에는 채굴자가 증가하면 블록이 너무 빨리 생성되고, 감소하면 너무 느리게 생성되었습니다.
이제는 난이도를 자동으로 조절하여 항상 일정한 속도를 유지할 수 있습니다. 이 메커니즘의 핵심 특징은 첫째, 완전히 자동화되어 중앙 통제가 필요 없다는 것입니다.
둘째, 과거 블록 생성 시간을 기반으로 계산하므로 객관적입니다. 셋째, 급격한 변화를 방지하는 안전장치가 내장되어 있습니다.
이러한 특징들이 블록체인의 탈중앙화와 안정성을 동시에 보장합니다.
코드 예제
// 난이도 조절 간격 상수 정의
const DIFFICULTY_ADJUSTMENT_INTERVAL = 2016; // 블록 개수
const BLOCK_GENERATION_INTERVAL = 10; // 목표 생성 시간 (초 단위)
// 난이도 재조정 여부 확인 함수
function shouldAdjustDifficulty(
latestBlock: Block,
blockchain: Block[]
): boolean {
// 최신 블록 인덱스가 조절 간격으로 나누어떨어지는지 확인
return (
latestBlock.index !== 0 &&
latestBlock.index % DIFFICULTY_ADJUSTMENT_INTERVAL === 0
);
}
설명
이것이 하는 일: 이 코드는 언제 난이도를 재조정해야 하는지 판단하는 핵심 로직입니다. 블록체인의 안정성을 유지하는 첫 번째 단계입니다.
첫 번째로, DIFFICULTY_ADJUSTMENT_INTERVAL 상수는 2016으로 설정되어 있습니다. 이는 비트코인의 실제 설정과 동일한 값으로, 2주마다 난이도를 재조정한다는 의미입니다.
BLOCK_GENERATION_INTERVAL은 10초로, 각 블록이 평균적으로 10초마다 생성되어야 한다는 목표 시간입니다. (실제 비트코인은 10분이지만 우리 예제에서는 10초로 설정) 두 번째로, shouldAdjustDifficulty 함수는 두 가지 조건을 확인합니다.
첫째, 제네시스 블록(index 0)이 아닌지 확인합니다. 제네시스 블록에서는 난이도 조절이 필요 없기 때문입니다.
둘째, 현재 블록 인덱스를 2016으로 나눈 나머지가 0인지 확인합니다. 이는 정확히 2016의 배수 위치에 있다는 뜻입니다.
세 번째로, 이 함수가 true를 반환하면 난이도 재조정 로직이 실행됩니다. false를 반환하면 이전 블록과 동일한 난이도를 유지합니다.
이렇게 주기적으로만 조정함으로써 네트워크가 일시적인 해시파워 변동에 과민 반응하지 않게 됩니다. 마지막으로, 이 함수의 결과를 통해 블록체인은 자율적으로 난이도를 관리할 수 있습니다.
중앙 서버나 관리자의 개입 없이도 수천 개의 노드가 동일한 판단을 내리게 되는 것이죠. 이것이 바로 탈중앙화된 합의의 아름다움입니다.
여러분이 이 코드를 사용하면 블록체인이 자동으로 네트워크 상태를 모니터링하고 적절한 시점에 난이도를 조정하게 됩니다. 이를 통해 안정적이고 예측 가능한 블록 생성 주기를 유지할 수 있으며, 사용자들에게 일관된 거래 처리 경험을 제공할 수 있습니다.
실전 팁
💡 2016이라는 숫자는 임의로 선택된 것이 아닙니다. 너무 짧으면 일시적 변동에 민감하고, 너무 길면 실제 해시파워 변화에 늦게 반응합니다. 실제 프로젝트에서는 네트워크 특성에 맞게 조정하세요.
💡 테스트 환경에서는 DIFFICULTY_ADJUSTMENT_INTERVAL을 10 정도로 낮춰서 난이도 조절을 빠르게 확인할 수 있습니다. 프로덕션에서는 반드시 원래 값으로 되돌리세요.
💡 블록 인덱스는 0부터 시작하므로 첫 난이도 조정은 2016번째 블록(인덱스 2016)이 아니라 2015번째 블록(인덱스 2015) 이후입니다. 오프바이원 에러를 조심하세요.
💡 이 함수는 순수 함수로 설계되어 있어 부작용이 없습니다. 테스트하기 쉽고 예측 가능한 코드입니다.
2. 실제 소요 시간 계산 - 난이도 조절의 기준 데이터
시작하며
난이도를 조절하려면 먼저 "실제로 블록들이 얼마나 빨리 생성되었는지" 알아야 합니다. 목표는 10분인데 실제로는 8분이 걸렸다면 난이도를 높여야 하고, 12분이 걸렸다면 낮춰야겠죠.
하지만 단순히 "마지막 블록 하나"의 생성 시간만 보면 안 됩니다. 한 블록은 우연히 빨리 찾을 수도, 늦게 찾을 수도 있습니다.
우리에게 필요한 건 "전체 2016개 블록"이 생성되는 데 걸린 총 시간입니다. 바로 이럴 때 필요한 것이 타임스탬프 기반 시간 계산입니다.
첫 번째 블록과 마지막 블록의 타임스탬프 차이를 구하면, 정확한 소요 시간을 알 수 있습니다.
개요
간단히 말해서, 실제 소요 시간은 2016개 블록 주기의 첫 블록과 마지막 블록 사이의 타임스탬프 차이입니다. 이 계산이 정확해야 하는 이유는 난이도 조절의 정확성이 여기에 달려있기 때문입니다.
1초의 오차도 누적되면 큰 차이를 만들 수 있습니다. 예를 들어, 실제로는 12일 걸렸는데 14일로 잘못 계산하면 난이도가 잘못 조정되어 블록 생성 속도가 계속 불안정해집니다.
기존에는 각 블록의 생성 시간을 일일이 더하는 방식으로 계산했다면, 이제는 단순히 첫 블록과 마지막 블록의 타임스탬프만 비교하면 됩니다. 훨씬 효율적이고 간단합니다.
이 계산의 핵심 특징은 첫째, O(1) 시간 복잡도로 매우 빠릅니다. 둘째, 모든 노드가 동일한 블록체인을 보면 동일한 결과를 얻습니다.
셋째, 과거 데이터를 기반으로 하므로 조작이 불가능합니다. 이러한 특징들이 공정하고 효율적인 난이도 조절을 가능하게 합니다.
코드 예제
// 실제 소요 시간 계산 함수
function getAdjustmentBlock(blockchain: Block[]): Block {
// 2016개 블록 전의 블록을 찾아 반환
const adjustmentBlockIndex =
blockchain.length - DIFFICULTY_ADJUSTMENT_INTERVAL;
return blockchain[adjustmentBlockIndex];
}
function calculateTimeExpected(): number {
// 예상 소요 시간 계산: 블록 개수 × 목표 생성 시간
return (
DIFFICULTY_ADJUSTMENT_INTERVAL * BLOCK_GENERATION_INTERVAL
);
}
function calculateTimeActual(
latestBlock: Block,
adjustmentBlock: Block
): number {
// 실제 소요 시간: 최신 블록과 조정 블록의 타임스탬프 차이
return latestBlock.timestamp - adjustmentBlock.timestamp;
}
설명
이것이 하는 일: 이 세 개의 함수는 협력하여 난이도 조절에 필요한 시간 데이터를 제공합니다. 실제 걸린 시간과 목표 시간을 비교할 수 있게 해줍니다.
첫 번째로, getAdjustmentBlock 함수는 정확히 2016개 블록 이전의 블록을 찾아냅니다. blockchain.length에서 DIFFICULTY_ADJUSTMENT_INTERVAL을 빼면 그 인덱스를 얻을 수 있습니다.
예를 들어, 현재 블록체인에 4032개의 블록이 있다면 4032 - 2016 = 2016번째 블록(인덱스 2016)을 반환합니다. 이 블록이 바로 현재 난이도 주기의 시작점입니다.
두 번째로, calculateTimeExpected 함수는 "이상적으로 걸려야 할 시간"을 계산합니다. 2016개 블록 × 10초 = 20160초입니다.
이것이 우리의 목표 기준선입니다. 실제 비트코인에서는 2016 × 600초 = 1,209,600초(2주)가 되겠죠.
세 번째로, calculateTimeActual 함수는 "실제로 걸린 시간"을 계산합니다. 최신 블록의 타임스탬프에서 2016개 전 블록의 타임스탬프를 빼면 됩니다.
이 연산은 매우 간단하지만 강력합니다. 예를 들어, 최신 블록이 1000000초이고 조정 블록이 980000초라면, 실제로는 20000초가 걸린 것입니다.
마지막으로, 이 세 함수의 결과를 조합하면 "실제 시간 / 예상 시간" 비율을 구할 수 있습니다. 위 예제에서는 20000 / 20160 ≈ 0.992로, 목표보다 약간 빨랐다는 것을 알 수 있습니다.
이 비율이 1보다 작으면 블록이 빨리 생성된 것이므로 난이도를 높여야 하고, 1보다 크면 느리게 생성된 것이므로 난이도를 낮춰야 합니다. 여러분이 이 코드를 사용하면 블록체인이 자신의 성능을 정확하게 측정할 수 있습니다.
이는 마치 운동선수가 자신의 기록을 재는 것과 같습니다. 정확한 측정 없이는 개선도 없기 때문에, 이 코드는 자율 조절의 핵심입니다.
실전 팁
💡 타임스탬프는 초 단위로 저장하는 것이 일반적입니다. 밀리초를 사용하면 더 정확하지만 오버플로우 위험이 있으니 주의하세요.
💡 블록체인 배열의 길이가 DIFFICULTY_ADJUSTMENT_INTERVAL보다 작을 때는 조정을 시도하지 않도록 shouldAdjustDifficulty에서 이미 걸러집니다. 방어적 프로그래밍을 위해 여기서도 체크를 추가할 수 있습니다.
💡 실제 소요 시간이 음수가 나오면 타임스탬프 조작이 있었다는 신호입니다. 이런 경우를 감지하는 검증 로직을 추가하면 보안이 강화됩니다.
💡 테스트할 때는 Mock 블록을 만들어 다양한 시나리오를 시뮬레이션하세요. 예상보다 2배 빠른 경우, 2배 느린 경우 등을 테스트하면 버그를 미리 발견할 수 있습니다.
💡 성능 최적화: 조정 블록을 매번 계산하는 대신 캐싱할 수도 있지만, 블록체인의 불변성 특성상 크게 필요하지 않습니다. 단순함을 유지하세요.
3. 새 난이도 계산 - 비율 기반 자동 조정
시작하며
이제 실제 시간과 목표 시간을 알았으니, 새로운 난이도를 계산할 차례입니다. 하지만 단순히 "시간이 절반이면 난이도를 2배로"라고 하면 위험합니다.
네트워크 공격이나 일시적 이상으로 극단적인 난이도 변화가 발생할 수 있기 때문입니다. 실제 비트코인은 이 문제를 해결하기 위해 매우 영리한 방법을 사용합니다.
한 번에 최대 4배까지만 증가하거나 1/4배까지만 감소할 수 있도록 제한을 둔 것입니다. 이렇게 하면 악의적인 공격자가 난이도를 극단적으로 조작하는 것을 막을 수 있습니다.
바로 이럴 때 필요한 것이 상한선과 하한선이 있는 비율 기반 난이도 계산입니다. 안전하면서도 효과적인 자동 조절이 가능해집니다.
개요
간단히 말해서, 새 난이도는 (현재 난이도 × 예상 시간 / 실제 시간)으로 계산되며, 변화 폭은 최대 4배로 제한됩니다. 이 공식이 중요한 이유는 수학적으로 정확하면서도 안전하기 때문입니다.
실제 시간이 예상의 절반이면 난이도가 2배가 되고, 실제 시간이 예상의 2배면 난이도가 절반이 됩니다. 완벽하게 비례하는 관계죠.
예를 들어, 채굴자가 갑자기 50% 증가했다면 블록이 약 33% 빨리 생성되고, 난이도는 그에 비례해서 증가합니다. 기존에는 고정된 난이도 증감을 사용했다면(예: 항상 +10%), 이제는 실제 성능에 정확히 비례하는 동적 조절을 할 수 있습니다.
훨씬 더 정밀하고 반응적입니다. 이 계산의 핵심 특징은 첫째, 비례적이라 공정합니다.
둘째, 상하한선으로 안전합니다. 셋째, 모든 노드가 동일한 계산을 하므로 합의가 자동으로 이루어집니다.
이러한 특징들이 블록체인을 자율적이면서도 안전하게 만듭니다.
코드 예제
function calculateNewDifficulty(
latestBlock: Block,
blockchain: Block[]
): number {
const adjustmentBlock = getAdjustmentBlock(blockchain);
const timeExpected = calculateTimeExpected();
const timeActual = calculateTimeActual(latestBlock, adjustmentBlock);
// 비율 계산: 예상 시간 / 실제 시간
let newDifficulty = adjustmentBlock.difficulty * (timeExpected / timeActual);
// 상한선 적용: 최대 4배까지만 증가
const upperBound = adjustmentBlock.difficulty * 4;
// 하한선 적용: 최대 1/4배까지만 감소
const lowerBound = adjustmentBlock.difficulty / 4;
// 범위 제한 적용
if (newDifficulty > upperBound) newDifficulty = upperBound;
if (newDifficulty < lowerBound) newDifficulty = lowerBound;
return newDifficulty;
}
설명
이것이 하는 일: 이 함수는 과거 데이터를 기반으로 미래의 적절한 난이도를 계산합니다. 안전장치가 내장된 자동 조절 시스템입니다.
첫 번째로, 필요한 모든 데이터를 수집합니다. adjustmentBlock은 2016개 전 블록이고, 그 블록의 difficulty가 현재 난이도입니다.
timeExpected는 20160초(우리 목표), timeActual은 실제로 걸린 시간입니다. 이 데이터들은 앞서 만든 함수들로부터 얻어집니다.
두 번째로, 핵심 공식을 적용합니다: newDifficulty = adjustmentBlock.difficulty * (timeExpected / timeActual). 만약 실제 시간이 16000초(목표보다 빠름)였다면, 비율은 20160/16000 = 1.26이 되어 난이도가 26% 증가합니다.
반대로 실제 시간이 24000초(목표보다 느림)였다면, 비율은 20160/24000 = 0.84가 되어 난이도가 16% 감소합니다. 이렇게 자동으로 균형을 맞춥니다.
세 번째로, 안전장치를 적용합니다. upperBound는 현재 난이도의 4배, lowerBound는 1/4배로 설정됩니다.
만약 계산된 난이도가 이 범위를 벗어나면 강제로 범위 내로 조정됩니다. 이는 악의적인 타임스탬프 조작이나 네트워크 이상으로부터 보호합니다.
네 번째로, 최종 난이도를 반환합니다. 이 값은 다음 2016개 블록 동안 사용될 난이도가 됩니다.
모든 정직한 노드는 동일한 블록체인을 보고 있으므로 동일한 난이도를 계산하게 됩니다. 이것이 바로 탈중앙화된 합의의 마법입니다.
여러분이 이 코드를 사용하면 블록체인이 스스로 최적의 난이도를 찾아갑니다. 채굴자가 늘어나면 자동으로 어려워지고, 줄어들면 자동으로 쉬워집니다.
마치 항온기처럼 항상 목표 온도(10분 블록 시간)를 유지하려고 노력하는 것입니다.
실전 팁
💡 4배 제한은 비트코인의 실제 값입니다. 다른 블록체인들은 2배나 8배를 사용하기도 합니다. 여러분의 네트워크 특성에 맞게 조정하세요.
💡 나눗셈 연산 시 0으로 나누는 경우를 방지하려면 timeActual이 0인지 먼저 체크하세요. 실제로는 타임스탬프 검증에서 걸러지겠지만 방어적 프로그래밍이 좋습니다.
💡 부동소수점 연산의 정밀도 문제를 피하려면 BigInt나 고정소수점 라이브러리를 사용할 수 있습니다. 하지만 대부분의 경우 JavaScript의 Number로 충분합니다.
💡 디버깅 시에는 console.log로 timeExpected, timeActual, 비율, 최종 난이도를 출력해보세요. 난이도가 왜 그렇게 변했는지 이해하기 쉬워집니다.
💡 단위 테스트에서는 경계값을 테스트하세요: 정확히 4배 빠른 경우, 정확히 4배 느린 경우, 정확히 목표 시간인 경우 등을 모두 커버해야 합니다.
4. 난이도와 해시 비교 - 채굴 성공 판정의 핵심
시작하며
난이도를 계산했으니 이제 "이 블록이 정말 유효한가?"를 판단해야 합니다. 채굴자가 "저 어려운 문제를 풀었어요!"라고 주장하는데, 정말 그런지 검증하는 것이죠.
비트코인에서는 이를 매우 간단하면서도 확실한 방법으로 합니다. 블록의 해시값을 16진수로 변환했을 때, 앞에 특정 개수의 0이 있어야 한다는 규칙입니다.
난이도가 높을수록 더 많은 0이 필요하죠. 바로 이럴 때 필요한 것이 해시와 난이도 비교 메커니즘입니다.
이를 통해 채굴의 성공과 실패를 객관적으로 판정할 수 있습니다.
개요
간단히 말해서, 블록 해시를 16진수로 변환한 값이 난이도 목표값보다 작거나 같으면 유효한 블록입니다. 이 검증이 중요한 이유는 작업 증명(Proof of Work)의 핵심이기 때문입니다.
채굴자는 이 조건을 만족하는 nonce를 찾기 위해 수백만 번의 시도를 합니다. 조건을 만족하는 해시를 찾는 것은 어렵지만, 찾았는지 검증하는 것은 단 한 번의 계산으로 가능합니다.
예를 들어, 난이도가 4라면 해시가 "0000abc..."처럼 시작해야 하고, 이를 확인하는 건 1초도 안 걸립니다. 기존에는 단순히 "문자열의 앞 n자리가 0인가?"를 확인했다면, 이제는 16진수를 정수로 변환하여 수학적으로 비교합니다.
더 정확하고 일관성 있는 검증이 가능합니다. 이 메커니즘의 핵심 특징은 첫째, 비대칭성입니다.
찾기는 어렵지만 검증은 쉽습니다. 둘째, 결정론적입니다.
같은 입력은 항상 같은 결과를 냅니다. 셋째, 조정 가능합니다.
난이도 값만 바꾸면 쉽게 어려워지거나 쉬워집니다. 이러한 특징들이 안전하고 효율적인 블록체인을 만듭니다.
코드 예제
function hexToBinary(hex: string): string {
// 16진수 문자열을 2진수 문자열로 변환
return hex
.split('')
.map(h => parseInt(h, 16).toString(2).padStart(4, '0'))
.join('');
}
function hashMatchesDifficulty(
hash: string,
difficulty: number
): boolean {
// 해시를 2진수로 변환
const hashInBinary = hexToBinary(hash);
// 난이도만큼의 0으로 시작하는 문자열 생성
const requiredPrefix = '0'.repeat(difficulty);
// 해시가 요구되는 접두사로 시작하는지 확인
return hashInBinary.startsWith(requiredPrefix);
}
설명
이것이 하는 일: 이 두 함수는 협력하여 블록의 유효성을 수학적으로 검증합니다. 작업 증명의 핵심 검증 로직입니다.
첫 번째로, hexToBinary 함수는 16진수 해시를 2진수로 변환합니다. 예를 들어, "a3f"는 "101000111111"이 됩니다.
이 과정에서 각 16진수 문자(0-f)를 4비트 2진수로 변환합니다. padStart(4, '0')를 사용하는 이유는 "a"(10)가 "1010"이 되어야 하는데 "1010"이 아니라 "1010"으로 4자리를 맞춰야 하기 때문입니다.
만약 패딩을 안 하면 "0"이 "0"이 되어 정확한 비교가 불가능합니다. 두 번째로, hashMatchesDifficulty 함수가 실제 검증을 수행합니다.
먼저 해시를 2진수로 변환합니다. 그 다음 requiredPrefix로 "000..."(난이도만큼의 0)을 생성합니다.
난이도가 4면 "0000", 10이면 "0000000000"이 됩니다. 이것이 바로 채굴자가 찾아야 하는 조건입니다.
세 번째로, startsWith 메서드로 간단하게 검증합니다. 2진수 해시가 요구되는 0들로 시작하면 true, 아니면 false를 반환합니다.
예를 들어, 난이도가 4이고 해시가 "0000101..."이면 통과하지만, "0001101..."이면 실패합니다. 단 한 비트 차이로 결정되는 엄격한 시스템입니다.
네 번째로, 이 검증이 통과되어야 블록이 블록체인에 추가될 수 있습니다. 모든 노드가 이 함수로 검증하므로, 부정한 블록은 네트워크에서 즉시 거부됩니다.
채굴자가 1000번 시도해서 999번 실패하더라도, 1번만 성공하면 그 블록은 영원히 유효합니다. 여러분이 이 코드를 사용하면 블록체인이 자동으로 모든 블록의 유효성을 검증할 수 있습니다.
사람이 일일이 확인할 필요 없이, 코드가 수학적으로 정확하게 판단합니다. 이것이 바로 "코드가 법"인 블록체인의 핵심입니다.
실전 팁
💡 16진수 대신 2진수를 사용하는 이유는 난이도를 더 세밀하게 조절할 수 있기 때문입니다. 16진수로 하면 4의 배수로만 난이도를 설정할 수 있지만, 2진수는 1씩 조절이 가능합니다.
💡 성능 최적화: 전체 해시를 2진수로 변환하지 않고, 필요한 앞부분만 변환할 수도 있습니다. 난이도가 16이면 앞 4개의 16진수 문자만 변환하면 충분합니다.
💡 테스트할 때는 알려진 해시와 난이도 쌍을 사용하세요. 예: "0000..." 해시는 난이도 4에 통과해야 하고, "ffff..." 해시는 난이도 1에도 실패해야 합니다.
💡 padStart를 빼먹으면 미묘한 버그가 생깁니다. 예를 들어 "00ab"가 "01010111"이 아니라 "01011"이 되어 검증이 틀어집니다. 항상 4자리로 패딩하세요.
💡 실제 비트코인은 BigInt를 사용하여 256비트 정수로 비교합니다. 더 효율적이지만 복잡합니다. 교육용으로는 문자열 비교가 이해하기 쉽습니다.
5. 블록 생성 시 난이도 결정 - 체인에 새 블록 추가하기
시작하며
지금까지 난이도를 언제, 어떻게 조절하는지 배웠습니다. 이제 실제로 새 블록을 생성할 때 이 모든 로직을 어떻게 적용하는지 알아봅시다.
새 블록을 만들 때마다 "이 블록의 난이도는 얼마여야 하지?"라는 질문에 답해야 합니다. 2016번째 블록이면 새로 계산하고, 아니면 이전 블록과 동일하게 유지해야 합니다.
바로 이럴 때 필요한 것이 getDifficulty 함수입니다. 모든 경우를 처리하는 통합 난이도 결정 로직입니다.
개요
간단히 말해서, getDifficulty 함수는 현재 블록체인 상태를 보고 다음 블록의 난이도를 자동으로 결정합니다. 이 함수가 중요한 이유는 블록 생성의 모든 경로에서 일관된 난이도를 보장하기 때문입니다.
채굴자가 블록을 만들 때도, 다른 노드가 블록을 검증할 때도 동일한 함수를 사용합니다. 예를 들어, 10명의 채굴자가 동시에 2016번째 블록을 만들려고 해도, 모두 동일한 난이도를 계산하게 됩니다.
기존에는 채굴자마다 다른 로직을 사용할 수 있었다면, 이제는 단일한 함수로 통일됩니다. 합의 오류의 여지가 사라집니다.
이 함수의 핵심 특징은 첫째, 조건부 로직으로 두 가지 경우를 처리합니다. 둘째, 순수 함수로 부작용이 없습니다.
셋째, 모든 노드가 동일한 결과를 얻습니다. 이러한 특징들이 탈중앙화된 합의를 가능하게 합니다.
코드 예제
function getDifficulty(blockchain: Block[]): number {
// 최신 블록 가져오기
const latestBlock = blockchain[blockchain.length - 1];
// 난이도 조절 시점인지 확인
if (shouldAdjustDifficulty(latestBlock, blockchain)) {
// 새 난이도 계산 및 반환
return calculateNewDifficulty(latestBlock, blockchain);
}
// 조절 시점이 아니면 이전 블록의 난이도 유지
return latestBlock.difficulty;
}
// 새 블록 생성 시 사용 예시
function generateNextBlock(
blockData: string,
blockchain: Block[]
): Block {
const previousBlock = blockchain[blockchain.length - 1];
const nextIndex = previousBlock.index + 1;
const nextTimestamp = Math.floor(Date.now() / 1000);
const nextDifficulty = getDifficulty(blockchain);
// ... 나머지 블록 생성 로직
}
설명
이것이 하는 일: 이 함수는 난이도 조절의 모든 로직을 하나로 통합하여 간단한 인터페이스를 제공합니다. 블록 생성의 핵심 결정을 담당합니다.
첫 번째로, 최신 블록을 가져옵니다. blockchain[blockchain.length - 1]은 가장 마지막에 추가된 블록, 즉 현재 체인의 끝입니다.
이 블록이 현재 상태를 대표하며, 모든 결정의 기준점이 됩니다. 두 번째로, shouldAdjustDifficulty로 조절 시점인지 확인합니다.
이 함수는 앞서 만든 것으로, 블록 인덱스가 2016의 배수인지 체크합니다. 만약 true가 반환되면 난이도 재계산이 필요한 시점입니다.
false면 그냥 현재 난이도를 계속 사용하면 됩니다. 세 번째로, 조절 시점이면 calculateNewDifficulty를 호출합니다.
이 함수는 지난 2016개 블록의 생성 시간을 분석하여 새로운 적절한 난이도를 계산합니다. 반환된 값은 다음 2016개 블록 동안 사용될 난이도입니다.
조절 시점이 아니면 단순히 latestBlock.difficulty를 반환하여 현재 난이도를 유지합니다. 네 번째로, generateNextBlock 예시에서 보듯이 이 함수는 실제 블록 생성 과정에 통합됩니다.
nextDifficulty를 구하는 것이 단 한 줄로 끝납니다. 개발자는 복잡한 난이도 로직을 신경 쓸 필요 없이, getDifficulty만 호출하면 됩니다.
이것이 좋은 추상화의 힘입니다. 여러분이 이 코드를 사용하면 블록 생성 로직이 매우 간결해집니다.
난이도 조절의 복잡한 세부사항은 모두 숨겨지고, 간단한 함수 호출만 남습니다. 이는 코드의 가독성과 유지보수성을 크게 향상시킵니다.
실전 팁
💡 이 함수는 매우 자주 호출되므로 성능이 중요합니다. 하지만 조절 시점이 아닐 때는 O(1)로 즉시 반환되므로 걱정할 필요 없습니다.
💡 테스트에서는 블록체인의 길이를 2015, 2016, 2017로 설정하여 경계값을 확인하세요. 2015에서는 이전 난이도, 2016에서는 새 난이도를 반환해야 합니다.
💡 제네시스 블록의 난이도는 보통 하드코딩됩니다. 이 함수는 제네시스 블록 이후부터 적용됩니다.
💡 디버깅 시에는 난이도가 변경되는 순간을 로깅하면 유용합니다. "Difficulty adjusted from X to Y at block N" 같은 메시지를 출력하세요.
💡 이 함수를 Memoization으로 최적화할 수도 있지만, 블록체인이 변경될 때마다 캐시를 무효화해야 하므로 복잡도가 증가합니다. 단순함을 유지하는 것이 좋습니다.
6. 채굴 루프와 난이도 - nonce 찾기 과정
시작하며
난이도를 어떻게 결정하는지 배웠으니, 이제 채굴자가 실제로 어떻게 유효한 블록을 찾는지 알아봅시다. 여러분이 채굴자라면 수백만 번의 시도를 통해 조건을 만족하는 nonce를 찾아야 합니다.
이 과정은 마치 복권처럼 운에 좌우됩니다. 하지만 완전히 무작위는 아닙니다.
난이도가 높을수록 당첨 확률이 낮아지고, 평균적으로 더 많은 시도가 필요합니다. 비트코인 네트워크 전체는 초당 수백 엑사해시(10^18)의 계산을 수행합니다.
바로 이럴 때 필요한 것이 체계적인 채굴 루프입니다. 효율적으로 nonce를 탐색하면서도 성공 조건을 정확히 확인해야 합니다.
개요
간단히 말해서, 채굴 루프는 nonce를 0부터 시작하여 유효한 해시를 찾을 때까지 증가시키며 시도하는 반복문입니다. 이 루프가 중요한 이유는 작업 증명의 "작업" 부분이 바로 여기서 일어나기 때문입니다.
CPU나 GPU가 전력을 소모하며 계산하는 것이 바로 이 루프입니다. 예를 들어, 난이도가 20이면 평균적으로 2^20(약 100만) 번의 시도가 필요하고, 난이도가 30이면 2^30(약 10억) 번이 필요합니다.
지수적으로 증가하는 것입니다. 기존에는 고정된 횟수만큼 시도하고 포기했다면, 이제는 성공할 때까지 무한정 시도합니다.
물론 실제로는 타임아웃이나 중단 조건을 추가하지만, 기본 원리는 "찾을 때까지"입니다. 이 루프의 핵심 특징은 첫째, 결정론적이지만 예측 불가능합니다.
같은 데이터로 시작하면 같은 nonce를 찾지만, 언제 찾을지는 알 수 없습니다. 둘째, 병렬화가 가능합니다.
여러 채굴자가 동시에 다른 nonce 범위를 탐색할 수 있습니다. 셋째, 검증은 단 한 번의 해시 계산으로 끝납니다.
이러한 특징들이 공정하고 효율적인 채굴을 가능하게 합니다.
코드 예제
function findBlock(
index: number,
previousHash: string,
timestamp: number,
data: string,
difficulty: number
): Block {
let nonce = 0;
// 유효한 해시를 찾을 때까지 반복
while (true) {
const hash = calculateHash(
index,
previousHash,
timestamp,
data,
difficulty,
nonce
);
// 해시가 난이도 조건을 만족하는지 확인
if (hashMatchesDifficulty(hash, difficulty)) {
// 성공! 블록 반환
return new Block(index, hash, previousHash, timestamp, data, difficulty, nonce);
}
// 실패하면 nonce 증가 후 재시도
nonce++;
}
}
설명
이것이 하는 일: 이 함수는 작업 증명의 "작업"을 실제로 수행합니다. 유효한 블록을 찾을 때까지 무한정 시도하는 채굴의 핵심입니다.
첫 번째로, nonce를 0으로 초기화합니다. 이것이 우리의 시작점입니다.
일부 채굴자는 무작위 값으로 시작하기도 하지만, 0부터 시작하는 것이 가장 단순하고 재현 가능한 방법입니다. 이론적으로는 어디서 시작하든 평균 시도 횟수는 같습니다.
두 번째로, while(true) 무한 루프를 시작합니다. 이것이 채굴의 본질입니다.
성공할 때까지 절대 포기하지 않습니다. 실제 구현에서는 타임아웃이나 새로운 블록 발견 시 중단하는 로직을 추가하지만, 기본 개념은 이렇게 단순합니다.
세 번째로, 현재 nonce로 해시를 계산합니다. calculateHash 함수는 블록의 모든 데이터(인덱스, 이전 해시, 타임스탬프, 데이터, 난이도, nonce)를 입력받아 SHA256 해시를 생성합니다.
이 계산이 CPU 사용의 대부분을 차지합니다. 난이도가 20이면 평균 100만 번 이 계산을 해야 합니다.
네 번째로, hashMatchesDifficulty로 성공 여부를 확인합니다. 앞서 만든 이 함수는 해시가 난이도만큼의 0으로 시작하는지 체크합니다.
만약 true가 반환되면 드디어 유효한 블록을 찾은 것입니다! 새로운 Block 객체를 생성하여 반환하고 루프를 종료합니다.
다섯 번째로, 조건을 만족하지 못하면 nonce++로 다음 시도를 준비합니다. 0, 1, 2, 3...
순차적으로 증가하며 모든 가능성을 탐색합니다. 이론적으로 nonce는 0부터 2^32-1(약 42억)까지 가능하지만, 대부분은 훨씬 작은 값에서 찾게 됩니다.
여러분이 이 코드를 사용하면 실제로 채굴을 체험할 수 있습니다. 난이도를 10으로 설정하고 실행하면 거의 즉시 완료되지만, 25로 설정하면 몇 초가 걸립니다.
30이면 몇 분, 40이면 몇 시간이 걸릴 수 있습니다. 이것이 바로 작업 증명의 아름다움입니다 - 조정 가능한 어려움입니다.
실전 팁
💡 실제 채굴에서는 타임스탬프를 주기적으로 업데이트합니다. 같은 타임스탬프로 계속 시도하면 nonce가 고갈될 수 있기 때문입니다. 1000번마다 타임스탬프를 갱신하는 것이 일반적입니다.
💡 성능 향상: calculateHash를 인라인으로 작성하면 함수 호출 오버헤드를 줄일 수 있습니다. 하지만 가독성이 떨어지므로 프로파일링 후 필요할 때만 최적화하세요.
💡 nonce가 Number.MAX_SAFE_INTEGER를 넘어가면 정밀도 문제가 발생합니다. BigInt를 사용하거나, 타임스탬프를 변경하는 방식으로 해결하세요.
💡 디버깅 시에는 10000번마다 진행 상황을 출력하면 좋습니다. "Tried 10000 nonces, current hash: ..." 같은 로그로 채굴이 진행되는지 확인할 수 있습니다.
💡 테스트에서는 난이도를 1이나 2로 낮춰서 빠르게 완료되도록 하세요. CI/CD 환경에서 오래 걸리는 테스트는 문제입니다.
7. 블록체인 검증과 난이도 - 전체 체인의 유효성 확인
시작하며
새로운 블록체인을 받았을 때, 우리는 그것이 정말 유효한지 확인해야 합니다. 다른 노드가 보내준 체인이 조작되지 않았는지, 모든 블록이 올바른 난이도로 채굴되었는지 검증하는 것입니다.
단순히 해시만 확인하는 것으로는 부족합니다. 각 블록이 당시의 올바른 난이도를 사용했는지도 확인해야 합니다.
만약 악의적인 채굴자가 난이도를 낮춰서 블록을 쉽게 만들었다면, 그 체인은 거부되어야 합니다. 바로 이럴 때 필요한 것이 난이도를 포함한 종합적인 블록체인 검증입니다.
모든 규칙을 빠짐없이 체크하는 엄격한 검증자가 되어야 합니다.
개요
간단히 말해서, 블록체인 검증은 각 블록의 해시, 이전 해시 연결, 타임스탬프, 그리고 난이도를 모두 확인하는 과정입니다. 이 검증이 중요한 이유는 블록체인의 불변성과 보안의 기초이기 때문입니다.
하나의 블록이라도 잘못되면 전체 체인을 거부해야 합니다. 예를 들어, 1000개 블록 중 500번째 블록의 난이도가 잘못되었다면, 그 이후의 모든 블록도 무효입니다.
전부 아니면 전무(all or nothing)의 원칙입니다. 기존에는 부분적인 검증만 했다면, 이제는 모든 측면을 종합적으로 검증합니다.
더 안전하지만 계산 비용이 높아집니다. 이 검증의 핵심 특징은 첫째, 철저함입니다.
모든 블록을 하나하나 확인합니다. 둘째, 독립성입니다.
각 노드가 스스로 검증하여 신뢰를 분산합니다. 셋째, 결정론적입니다.
같은 체인은 항상 같은 검증 결과를 냅니다. 이러한 특징들이 탈중앙화된 신뢰를 구축합니다.
코드 예제
function isValidBlockchain(blockchain: Block[]): boolean {
// 제네시스 블록 확인
if (JSON.stringify(blockchain[0]) !== JSON.stringify(GENESIS_BLOCK)) {
console.log('Invalid genesis block');
return false;
}
// 모든 블록을 순차적으로 검증
for (let i = 1; i < blockchain.length; i++) {
const currentBlock = blockchain[i];
const previousBlock = blockchain[i - 1];
// 1. 이전 블록과의 연결 확인
if (currentBlock.previousHash !== previousBlock.hash) {
console.log(`Invalid previous hash at block ${i}`);
return false;
}
// 2. 현재 블록의 해시가 정확한지 확인
const calculatedHash = calculateHashForBlock(currentBlock);
if (currentBlock.hash !== calculatedHash) {
console.log(`Invalid hash at block ${i}`);
return false;
}
// 3. 난이도 조건을 만족하는지 확인
if (!hashMatchesDifficulty(currentBlock.hash, currentBlock.difficulty)) {
console.log(`Hash doesn't match difficulty at block ${i}`);
return false;
}
// 4. 난이도가 올바르게 설정되었는지 확인
const expectedDifficulty = getDifficulty(blockchain.slice(0, i));
if (currentBlock.difficulty !== expectedDifficulty) {
console.log(`Invalid difficulty at block ${i}`);
return false;
}
}
return true; // 모든 검증 통과
}
설명
이것이 하는 일: 이 함수는 블록체인 전체를 철저히 검증하는 보안 게이트입니다. 모든 블록이 규칙을 따르는지 확인하는 최후의 방어선입니다.
첫 번째로, 제네시스 블록을 확인합니다. 모든 정직한 노드는 동일한 제네시스 블록에서 시작해야 합니다.
JSON.stringify로 비교하는 이유는 객체의 모든 필드를 한 번에 비교하기 위함입니다. 만약 다르다면 완전히 다른 네트워크의 블록체인이므로 즉시 거부합니다.
두 번째로, for 루프로 모든 블록을 순회합니다. i는 1부터 시작하는데, 제네시스 블록(인덱스 0)은 이미 확인했기 때문입니다.
각 블록에 대해 현재 블록과 이전 블록을 동시에 참조하여 네 가지 검증을 수행합니다. 세 번째로, 첫 번째 검증은 체인의 연결성입니다.
currentBlock.previousHash가 실제로 previousBlock.hash와 일치하는지 확인합니다. 이것이 "블록체인"이라는 이름의 유래입니다 - 블록들이 해시로 사슬처럼 연결되어 있습니다.
만약 불일치하면 누군가 중간 블록을 조작했거나, 체인이 손상된 것입니다. 네 번째로, 두 번째 검증은 해시의 정확성입니다.
저장된 해시가 실제 계산 결과와 일치하는지 확인합니다. calculateHashForBlock으로 다시 계산하여 비교합니다.
만약 다르다면 블록 데이터가 변조되었거나, 해시 계산이 잘못된 것입니다. 다섯 번째로, 세 번째 검증은 작업 증명입니다.
hashMatchesDifficulty로 해시가 난이도 조건을 만족하는지 확인합니다. 이것이 채굴자가 실제로 작업을 했다는 증명입니다.
만약 만족하지 못하면 난이도를 우회하여 쉽게 블록을 만든 것입니다. 여섯 번째로, 네 번째 검증은 난이도의 적절성입니다.
getDifficulty로 그 시점에서 올바른 난이도를 계산하여 실제 난이도와 비교합니다. blockchain.slice(0, i)로 현재 블록 이전까지의 체인을 전달합니다.
만약 다르다면 난이도 조절 규칙을 위반한 것입니다. 마지막으로, 모든 블록이 모든 검증을 통과해야 true를 반환합니다.
중간에 하나라도 실패하면 즉시 false를 반환하고 종료됩니다. 이 엄격함이 블록체인의 보안을 보장합니다.
여러분이 이 코드를 사용하면 어떤 블록체인도 안전하게 검증할 수 있습니다. 인터넷에서 받은 체인이든, 다른 노드가 보낸 체인이든, 이 함수를 통과해야만 신뢰할 수 있습니다.
이것이 바로 "검증하되 신뢰하지 말라(Don't trust, verify)"는 블록체인의 모토입니다.
실전 팁
💡 검증은 CPU 집약적이므로 긴 체인에서는 시간이 걸립니다. 천만 개 블록이 있다면 검증에 몇 분이 걸릴 수 있습니다. 백그라운드에서 비동기로 실행하는 것을 고려하세요.
💡 에러 메시지에 블록 인덱스를 포함시키면 디버깅이 쉬워집니다. "Invalid hash at block 12345"처럼 정확한 위치를 알려줍니다.
💡 최적화: 이미 검증한 체인의 일부를 캐싱할 수 있습니다. 새 블록이 추가될 때마다 전체를 재검증할 필요 없이, 마지막 검증 이후의 블록만 확인하면 됩니다.
💡 단위 테스트에서는 의도적으로 잘못된 체인을 만들어 테스트하세요: 잘못된 해시, 잘못된 난이도, 끊어진 체인 등 다양한 오류 케이스를 커버해야 합니다.
💡 production 환경에서는 console.log 대신 적절한 로깅 라이브러리를 사용하고, 로그 레벨을 조정하여 성능 영향을 최소화하세요.
8. 난이도 폭발과 Ice Age - 비트코인과 이더리움의 차이점
시작하며
지금까지 비트코인 스타일의 난이도 조절을 배웠습니다. 하지만 이더리움은 한 가지 독특한 메커니즘을 더 추가했습니다 - 바로 "난이도 폭탄(Difficulty Bomb)" 또는 "Ice Age"입니다.
이더리움은 의도적으로 시간이 지날수록 난이도를 기하급수적으로 증가시켰습니다. 왜일까요?
채굴자들이 오래된 체인에 머물지 못하도록 강제하기 위해서입니다. 이더리움 2.0(PoS)로의 전환을 촉진하는 메커니즘이었죠.
바로 이럴 때 필요한 것이 시간 기반 난이도 증가 메커니즘입니다. 네트워크 업그레이드를 강제하는 독특한 인센티브 설계입니다.
개요
간단히 말해서, 난이도 폭탄은 블록 번호가 증가할수록 난이도를 지수적으로 증가시키는 메커니즘입니다. 이 메커니즘이 흥미로운 이유는 경제적 인센티브로 네트워크 업그레이드를 유도하기 때문입니다.
채굴자들은 점점 채굴이 어려워지므로, 새로운 버전으로 이동할 수밖에 없습니다. 예를 들어, 이더리움은 여러 차례 하드포크를 통해 난이도 폭탄을 연기했고, 최종적으로 PoS로 전환하며 완전히 제거했습니다.
비트코인에는 이런 메커니즘이 없습니다. 반대로 이더리움은 이를 통해 네트워크 업그레이드를 계획적으로 강제할 수 있었습니다.
이 메커니즘의 핵심 특징은 첫째, 점진적이지만 결국 압도적입니다. 처음에는 눈에 띄지 않지만 결국 채굴이 불가능해집니다.
둘째, 예측 가능합니다. 개발자와 채굴자 모두 언제쯤 문제가 될지 알 수 있습니다.
셋째, 사회적 합의를 코드로 구현한 것입니다. 이러한 특징들이 블록체인 거버넌스의 새로운 가능성을 보여줍니다.
코드 예제
// 이더리움 스타일의 난이도 폭탄 (단순화된 버전)
function calculateDifficultyWithBomb(
latestBlock: Block,
blockchain: Block[]
): number {
// 기본 난이도 계산 (비트코인 방식)
const baseDifficulty = calculateNewDifficulty(latestBlock, blockchain);
// 폭탄 시작 블록 번호 (예: 100만 블록부터)
const BOMB_START_BLOCK = 1000000;
// 폭탄 주기 (100,000 블록마다 2배)
const BOMB_PERIOD = 100000;
const blockNumber = latestBlock.index + 1;
// 폭탄이 시작되지 않았으면 기본 난이도 반환
if (blockNumber < BOMB_START_BLOCK) {
return baseDifficulty;
}
// 폭탄 계산: 2^(블록수 / 주기)
const bombPeriods = Math.floor((blockNumber - BOMB_START_BLOCK) / BOMB_PERIOD);
const bombMultiplier = Math.pow(2, bombPeriods);
// 기본 난이도에 폭탄 배수 적용
return baseDifficulty + bombMultiplier;
}
설명
이것이 하는 일: 이 함수는 일반적인 난이도 조절에 시간 기반 폭탄을 추가합니다. 네트워크를 의도적으로 느리게 만드는 독특한 메커니즘입니다.
첫 번째로, 기본 난이도를 계산합니다. calculateNewDifficulty로 비트코인 방식의 난이도 조절을 먼저 수행합니다.
이것이 기준선입니다. 폭탄은 여기에 "추가"로 적용됩니다.
두 번째로, 상수를 정의합니다. BOMB_START_BLOCK은 폭탄이 활성화되는 시점입니다.
100만 블록 이전에는 폭탄이 없습니다. BOMB_PERIOD는 난이도가 2배가 되는 주기입니다.
10만 블록마다 2배씩 증가합니다. 세 번째로, 현재 블록 번호를 확인합니다.
latestBlock.index + 1은 다음 블록의 인덱스입니다. 만약 BOMB_START_BLOCK보다 작으면 아직 폭탄이 작동하지 않으므로 baseDifficulty를 그대로 반환합니다.
네 번째로, 폭탄을 계산합니다. (blockNumber - BOMB_START_BLOCK) / BOMB_PERIOD로 몇 주기가 지났는지 계산합니다.
예를 들어, 현재 블록이 120만이면 (1200000 - 1000000) / 100000 = 2주기가 지난 것입니다. 그러면 bombMultiplier는 2^2 = 4배가 됩니다.
다섯 번째로, 최종 난이도를 반환합니다. baseDifficulty + bombMultiplier로 폭탄 효과를 더합니다.
주의할 점은 곱하기가 아니라 더하기입니다. 시간이 지날수록 bombMultiplier가 지배적이 되어 기본 난이도는 거의 의미가 없어집니다.
예를 들어, 10주기가 지나면 2^10 = 1024가 추가되는데, 기본 난이도가 10이라면 전체 난이도는 1034가 됩니다. 여러분이 이 코드를 사용하면 네트워크에 "만료일"을 설정할 수 있습니다.
채굴자들은 결국 새 버전으로 업그레이드해야 합니다. 이는 중앙화된 강제 없이도 네트워크 업그레이드를 달성하는 영리한 방법입니다.
실전 팁
💡 실제 이더리움의 난이도 폭탄은 훨씬 복잡합니다. 여러 번의 하드포크를 거치며 공식이 변경되었고, 연기되고, 조정되었습니다. 이 코드는 개념을 설명하는 단순화된 버전입니다.
💡 폭탄 주기를 너무 짧게 설정하면 준비 시간 없이 네트워크가 멈춥니다. 너무 길게 설정하면 압박이 없어 업그레이드가 지연됩니다. 신중한 균형이 필요합니다.
💡 테스트 환경에서는 BOMB_START_BLOCK을 100 정도로 낮춰서 빠르게 효과를 확인할 수 있습니다. 지수 증가가 얼마나 빠른지 놀랄 것입니다.
💡 난이도 폭탄은 논란의 여지가 있는 설계입니다. 채굴자에게 불리하고, 네트워크 거버넌스에 대한 철학적 논쟁을 일으킵니다. 도입하기 전에 커뮤니티와 충분히 논의하세요.
💡 Math.pow는 큰 수에서 정밀도 문제가 있을 수 있습니다. bombPeriods가 50을 넘으면 Number의 한계에 도달할 수 있으니 BigInt를 고려하세요.