PBFT에서 리더(Primary)가 문제가 생겼을 때 리더를 교체하는 프로토콜에 대한 학습 내용을 정리한다.
1. 기존 리더(node0) 문제 발생 (죽음/악의적 행동/응답 없음)
2. 타임아웃 발생
3. 각 노드가 StartViewChange() 호출 → ViewChange 메시지 브로드캐스트
4. 각 노드가 HandleViewChange()로 투표 수집
5. quorum(2f+1) 달성
6. 새 리더 후보(node1)가 CreateNewViewMsg() 호출
7. 새 리더가 NewViewMsg 브로드캐스트
8. 각 노드가 HandleNewView()로 검증 후 수락
9. 새 뷰로 전환 완료, 정상 합의 재개
뷰 체인지를 시작하는 함수. 각 노드가 호출한다.
하는 일:
- ViewChangeMsg 생성 (내 체크포인트, Prepared 블록들 정보 포함)
- 내 메시지 저장
- 브로드캐스트
다른 노드가 보낸 ViewChange 메시지를 처리하는 함수.
하는 일:
- 받은 메시지 저장 (맵에 nodeID를 키로)
- quorum 체크
- 2f+1개 모이면 true 반환
새 리더가 될 노드가 호출하는 함수.
하는 일:
- 수집한 ViewChange 메시지들 정리
- computePrePrepareSet으로 하다 만 블록들 계산
- NewViewMsg 생성
주의: 이 함수 호출한다고 바로 리더 되는 게 아니다. NewViewMsg를 보내고 다른 노드들이 수락해야 공식 리더가 된다.
새 리더가 보낸 NewViewMsg를 처리하는 함수.
하는 일:
- 메시지 검증 (ViewChangeMsgs가 2f+1개인지 등)
- currentView 업데이트
- inProgress = false로 변경
- 오래된 ViewChange 메시지 정리
- 완료 콜백 호출
이전 뷰에서 하다 만 블록들을 찾는 함수.
하는 일:
- 체크포인트 중 가장 높은 것 찾기 (확정된 마지막 블록)
- PreparedSet 중 가장 높은 것 찾기 (작업 중이던 마지막 블록)
- 그 사이 블록들의 PrePrepare 정보 수집
리더가 누구인지 나타내는 번호.
View 0 → node0이 리더
View 1 → node1이 리더
View 2 → node2가 리더
...
계산: View % 노드수 = 리더 인덱스
블록이 확정된 지점을 나타내는 것. 주기적으로 생성된다.
블록 100 완료 → 체크포인트 생성 (seq=100)
블록 200 완료 → 체크포인트 생성 (seq=200)
...
View와 다른 개념이다. 체크포인트는 블록 번호를 의미한다.
Prepared 상태인 블록들의 목록.
블록 상태 단계: Idle → PrePrepared → Prepared → Committed
PreparedSet에는 Prepared 상태인 것만 들어간다.
PrePrepared나 Committed는 들어가지 않는다.
합의에 필요한 최소 투표 수.
전체 노드 = 3f + 1 (f는 허용 가능한 비잔틴 노드 수)
quorum = 2f + 1
예: 노드 4개 → f=1 → quorum=3
뷰 체인지 진행 중인지 나타내는 플래그.
true: 뷰 체인지 중 → 정상 합의 멈춤
false: 뷰 체인지 끝남 → 정상 합의 재개 가능
StartViewChange()에서 true로 설정
HandleNewView()에서 false로 설정
Go에서 변수를 선언만 하면 자동으로 초기화된다.
var x int // x = 0
var s string // s = ""
var b bool // b = false
var p *int // p = nil중첩 맵에서 안쪽 맵이 nil이면 먼저 초기화해야 한다.
if m[key1] == nil {
m[key1] = make(map[string]*SomeType)
}
m[key1][key2] = value안 하면 panic 발생한다.
// 방법 1: 생성할 때 &
msg := &ViewChangeMsg{...} // msg는 *ViewChangeMsg
someMap[key] = msg // 그냥 대입
// 방법 2: 대입할 때 &
msg := ViewChangeMsg{...} // msg는 ViewChangeMsg
someMap[key] = &msg // & 붙여서 대입둘 다 결과는 같다. 보통 방법 1을 많이 쓴다.
for _, item := range items {
copy := item // 먼저 복사
someMap[key] = © // 복사본의 주소 저장
}직접 &item 하면 모든 항목이 같은 주소를 가리킬 수 있어서 위험하다.
for key := range m {
if condition {
delete(m, key)
}
}Go에서는 맵 순회 중 삭제해도 안전하다.
함수를 변수에 저장하고 나중에 호출한다.
type Manager struct {
callback func(int)
}
func (m *Manager) SetCallback(f func(int)) {
m.callback = f
}
func (m *Manager) DoSomething() {
if m.callback != nil {
m.callback(42)
}
}의존성을 줄이고 테스트하기 좋은 코드를 만들 수 있다.
sync.RWMutex 메서드:
- Lock() / Unlock(): 쓰기 락. 혼자만 접근 가능.
- RLock() / RUnlock(): 읽기 락. 여러 고루틴 동시 접근 가능.
읽기만 하면 RLock, 수정하면 Lock 사용.지정한 시간 후에 함수를 실행한다.
timer := time.AfterFunc(10*time.Second, func() {
fmt.Println("10초 지남!")
})
// 취소하려면
timer.Stop()별도 고루틴에서 비동기로 실행된다.
구조체를 JSON 바이트로 변환한다.
data, err := json.Marshal(someStruct)
// data = []byte(`{"field":"value",...}`)네트워크로 전송할 때 사용한다.
map[uint64]map[string]*ViewChangeMsg
↑ ↑ ↑
뷰번호 노드ID 메시지
예:
viewChangeMsgs[1]["node0"] = node0이 보낸 "View 1로 바꾸자" 메시지map[uint64]*PrePrepareMsg
↑ ↑
블록번호 블록정보
예:
preparedBlocks[301] = 블록301의 PrePrepare 정보- 같은 패키지(폴더) 내 파일들은 import 없이 서로 참조 가능하다.
- ViewChangeMsg, NewViewMsg 등은 messages.go에 정의되어 있다.