Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 46 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,51 @@
# CHANGELOG

## UNRELEASED

### DEPENDENCIES

### BUG FIXES

### IMPROVEMENTS

### FEATURES

### BUG-FIXES

### STATE-BREAKING

### API-BREAKING

## v0.38.19

*October 14, 2025*

This release fixes two security issues, including ([ASA-2025-003](https://github.com/cometbft/cometbft/security/advisories/GHSA-hrhf-2vcr-ghch)).
Users are encouraged to upgrade as soon as possible.

Additionally included is a bug fix to properly prune extended commits (with
vote extensions).

### BUG-FIXES

- `[consensus]` Reject oversized proposals
([\#5324](https://github.com/cometbft/cometbft/pull/5324))
- `[store]` Prune extended commits properly
([5275](https://github.com/cometbft/cometbft/issues/5275))
- `[bits]` Validate BitArray mismatched Bits and Elems length
([ASA-2025-003](https://github.com/cometbft/cometbft/security/advisories/GHSA-hrhf-2vcr-ghch))

## v0.38.18

*July 3, 2025*

Adds precommit metrics and reindex CLI command.

### IMPROVEMENTS

- Adds metrics that emit precommit data; precommit quorum delay from proposal, and precommit vote count and stake weight within timeout commit period.
([\#5251](https://github.com/cometbft/cometbft/issues/5251))

## v0.38.17

*February 3, 2025*
Expand Down Expand Up @@ -881,4 +927,3 @@ Friendly reminder, we have a [bug bounty program](https://hackerone.com/cosmos).
## Previous changes

For changes released before the creation of CometBFT, please refer to the Tendermint Core [CHANGELOG.md](https://github.com/tendermint/tendermint/blob/a9feb1c023e172b542c972605311af83b777855b/CHANGELOG.md).

9 changes: 9 additions & 0 deletions consensus/reactor.go
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,9 @@ func (m *NewValidBlockMessage) ValidateBasic() error {
if err := m.BlockPartSetHeader.ValidateBasic(); err != nil {
return fmt.Errorf("wrong BlockPartSetHeader: %v", err)
}
if err := m.BlockParts.ValidateBasic(); err != nil {
return fmt.Errorf("validating BlockParts: %w", err)
}
if m.BlockParts.Size() == 0 {
return errors.New("empty blockParts")
}
Expand Down Expand Up @@ -1871,6 +1874,9 @@ func (m *ProposalPOLMessage) ValidateBasic() error {
if m.ProposalPOLRound < 0 {
return errors.New("negative ProposalPOLRound")
}
if err := m.ProposalPOL.ValidateBasic(); err != nil {
return fmt.Errorf("validating ProposalPOL: %w", err)
}
if m.ProposalPOL.Size() == 0 {
return errors.New("empty ProposalPOL bit array")
}
Expand Down Expand Up @@ -2016,6 +2022,9 @@ func (m *VoteSetBitsMessage) ValidateBasic() error {
if err := m.BlockID.ValidateBasic(); err != nil {
return fmt.Errorf("wrong BlockID: %v", err)
}
if err := m.Votes.ValidateBasic(); err != nil {
return fmt.Errorf("validating Votes: %w", err)
}
// NOTE: Votes.Size() can be zero if the node does not have any
if m.Votes.Size() > types.MaxVotesCount {
return fmt.Errorf("votes bit array is too big: %d, max: %d", m.Votes.Size(), types.MaxVotesCount)
Expand Down
24 changes: 24 additions & 0 deletions consensus/reactor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,14 @@ func TestNewValidBlockMessageValidateBasic(t *testing.T) {
func(msg *NewValidBlockMessage) { msg.BlockParts = bits.NewBitArray(int(types.MaxBlockPartsCount) + 1) },
fmt.Sprintf("blockParts bit array size %d not equal to BlockPartSetHeader.Total 1", types.MaxBlockPartsCount+1),
},
{
func(msg *NewValidBlockMessage) { msg.BlockParts.Elems = nil },
"mismatch between specified number of bits 1, and number of elements 0, expected 1 elements",
},
{
func(msg *NewValidBlockMessage) { msg.BlockParts.Bits = 500 },
"mismatch between specified number of bits 500, and number of elements 1, expected 8 elements",
},
}

for i, tc := range testCases {
Expand Down Expand Up @@ -943,6 +951,14 @@ func TestProposalPOLMessageValidateBasic(t *testing.T) {
func(msg *ProposalPOLMessage) { msg.ProposalPOL = bits.NewBitArray(types.MaxVotesCount + 1) },
"proposalPOL bit array is too big: 10001, max: 10000",
},
{
func(msg *ProposalPOLMessage) { msg.ProposalPOL.Elems = nil },
"mismatch between specified number of bits 1, and number of elements 0, expected 1 elements",
},
{
func(msg *ProposalPOLMessage) { msg.ProposalPOL.Bits = 500 },
"mismatch between specified number of bits 500, and number of elements 1, expected 8 elements",
},
}

for i, tc := range testCases {
Expand Down Expand Up @@ -1099,6 +1115,14 @@ func TestVoteSetBitsMessageValidateBasic(t *testing.T) {
func(msg *VoteSetBitsMessage) { msg.Votes = bits.NewBitArray(types.MaxVotesCount + 1) },
"votes bit array is too big: 10001, max: 10000",
},
{
func(msg *VoteSetBitsMessage) { msg.Votes.Elems = nil },
"mismatch between specified number of bits 1, and number of elements 0, expected 1 elements",
},
{
func(msg *VoteSetBitsMessage) { msg.Votes.Bits = 500 },
"mismatch between specified number of bits 500, and number of elements 1, expected 8 elements",
},
}

for i, tc := range testCases {
Expand Down
32 changes: 28 additions & 4 deletions libs/bits/bit_array.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func NewBitArray(bits int) *BitArray {
}
return &BitArray{
Bits: bits,
Elems: make([]uint64, (bits+63)/64),
Elems: make([]uint64, numElements(bits)),
}
}

Expand All @@ -41,7 +41,7 @@ func NewBitArrayFromFn(bits int, fn func(int) bool) *BitArray {
}
bA := &BitArray{
Bits: bits,
Elems: make([]uint64, (bits+63)/64),
Elems: make([]uint64, numElements(bits)),
}
for i := 0; i < bits; i++ {
v := fn(i)
Expand Down Expand Up @@ -101,7 +101,7 @@ func (bA *BitArray) SetIndex(i int, v bool) bool {
}

func (bA *BitArray) setIndex(i int, v bool) bool {
if i >= bA.Bits {
if i >= bA.Bits || i/64 >= len(bA.Elems) {
return false
}
if v {
Expand Down Expand Up @@ -159,7 +159,7 @@ func (bA *BitArray) copy() *BitArray {
}

func (bA *BitArray) copyBits(bits int) *BitArray {
c := make([]uint64, (bits+63)/64)
c := make([]uint64, numElements(bits))
copy(c, bA.Elems)
return &BitArray{
Bits: bits,
Expand Down Expand Up @@ -354,6 +354,11 @@ func (bA *BitArray) GetTrueIndices() []int {
}

func (bA *BitArray) getNumTrueIndices() int {
if bA == nil || bA.Bits == 0 || len(bA.Elems) == 0 || len(bA.Elems) != numElements(bA.Bits) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh so the solution to avoiding the deadlock was to modify this line to avoid calling Size()

Ref: cometbft/cometbft@be5677c#diff-204903b7f426357ddfc87a0d0a8d49b3ec22f4a48a8bb3a7ce44f821d5ae9d96R285

// size and elements must be valid to do this calc
return 0
}

count := 0
numElems := len(bA.Elems)
// handle all elements except the last one
Expand Down Expand Up @@ -567,3 +572,22 @@ func (bA *BitArray) FromProto(protoBitArray *cmtprotobits.BitArray) {
bA.Elems = protoBitArray.Elems
}
}

// ValidateBasic validates a BitArray. Note that a nil BitArray and BitArray of
// size 0 bits is valid. However the number of Bits and Elems be valid based on
// each other.
func (bA *BitArray) ValidateBasic() error {
if bA == nil {
return nil
}

expectedElems := numElements(bA.Size())
if expectedElems != len(bA.Elems) {
return fmt.Errorf("mismatch between specified number of bits %d, and number of elements %d, expected %d elements", bA.Size(), len(bA.Elems), expectedElems)
}
return nil
}

func numElements(bits int) int {
return (bits + 63) / 64
}
90 changes: 89 additions & 1 deletion libs/bits/bit_array_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,28 @@ func TestGetNumTrueIndices(t *testing.T) {
}
}

func TestGetNumTrueIndicesInvalidStates(t *testing.T) {
testCases := []struct {
name string
bA1 *BitArray
exp int
}{
{"empty", &BitArray{}, 0},
{"explicit 0 bits nil elements", &BitArray{Bits: 0, Elems: nil}, 0},
{"explicit 0 bits 0 len elements", &BitArray{Bits: 0, Elems: make([]uint64, 0)}, 0},
{"nil", nil, 0},
{"with elements", NewBitArray(10), 0},
{"more elements than bits specifies", &BitArray{Bits: 0, Elems: make([]uint64, 5)}, 0},
{"less elements than bits specifies", &BitArray{Bits: 200, Elems: make([]uint64, 1)}, 0},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
n := tc.bA1.getNumTrueIndices()
require.Equal(t, n, tc.exp)
})
}
}

func TestGetNthTrueIndex(t *testing.T) {
type testcase struct {
Input string
Expand Down Expand Up @@ -226,7 +248,7 @@ func TestGetNthTrueIndex(t *testing.T) {
}
}

func TestBytes(_ *testing.T) {
func TestBytes(t *testing.T) {
bA := NewBitArray(4)
bA.SetIndex(0, true)
check := func(bA *BitArray, bz []byte) {
Expand All @@ -253,6 +275,10 @@ func TestBytes(_ *testing.T) {
check(bA, []byte{0x80, 0x01})
bA.SetIndex(9, true)
check(bA, []byte{0x80, 0x03})

bA = NewBitArray(4)
bA.Elems = nil
require.False(t, bA.SetIndex(1, true))
}

func TestEmptyFull(t *testing.T) {
Expand Down Expand Up @@ -477,6 +503,28 @@ func TestAddBitArray(t *testing.T) {
}
}

func TestBitArrayValidateBasic(t *testing.T) {
testCases := []struct {
name string
bA1 *BitArray
expPass bool
}{
{"valid empty", &BitArray{}, true},
{"valid explicit 0 bits nil elements", &BitArray{Bits: 0, Elems: nil}, true},
{"valid explicit 0 bits 0 len elements", &BitArray{Bits: 0, Elems: make([]uint64, 0)}, true},
{"valid nil", nil, true},
{"valid with elements", NewBitArray(10), true},
{"more elements than bits specifies", &BitArray{Bits: 0, Elems: make([]uint64, 5)}, false},
{"less elements than bits specifies", &BitArray{Bits: 200, Elems: make([]uint64, 1)}, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.bA1.ValidateBasic()
require.Equal(t, err == nil, tc.expPass)
})
}
}

// Tests that UnmarshalJSON doesn't crash when no bits are passed into the JSON.
// See issue https://github.com/cometbft/cometbft/issues/2658
func TestUnmarshalJSONDoesntCrashOnZeroBits(t *testing.T) {
Expand Down Expand Up @@ -541,3 +589,43 @@ func TestBitArray_Fill(t *testing.T) {
require.True(t, bA.IsFull())
})
}

func TestBitArraySize(t *testing.T) {
t.Run("Size returns 0 for nil BitArray", func(t *testing.T) {
var nilArray *BitArray
require.Equal(t, 0, nilArray.Size())
})

t.Run("Size returns the correct size for the BitArray", func(t *testing.T) {
sizes := []int{1, 10, 64, 65, 100, 1000}
for _, size := range sizes {
bitArray := NewBitArray(size)
require.Equal(t, size, bitArray.Size())
}
})

t.Run("Size returns the correct size when called concurrently", func(t *testing.T) {
const numGoroutines = 100
const numIterations = 100

want := 500
bitArray := NewBitArray(want)

results := make(chan int, numGoroutines*numIterations)

// Launch goroutines that only call Size() concurrently
for i := 0; i < numGoroutines; i++ {
go func() {
for j := 0; j < numIterations; j++ {
size := bitArray.Size()
results <- size
}
}()
}

for i := 0; i < numGoroutines*numIterations; i++ {
got := <-results
require.Equal(t, want, got)
}
})
}
Loading