Skip to content

Commit e5b42b4

Browse files
committed
Actually extract blockhash
1 parent d2405cb commit e5b42b4

11 files changed

Lines changed: 480 additions & 70 deletions

cmd/horcrux/cmd/config.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func initCmd() *cobra.Command {
4343
for threshold signer mode, --cosigner flags and --threshold flag are required.
4444
`,
4545
Args: cobra.NoArgs,
46-
RunE: func(cmd *cobra.Command, args []string) (err error) {
46+
RunE: func(cmd *cobra.Command, _ []string) (err error) {
4747
cmdFlags := cmd.Flags()
4848

4949
bare, _ := cmdFlags.GetBool(flagBare)

cmd/horcrux/cmd/leader_election.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ To choose a specific leader, pass that leader's ID as an argument.
3030
Example: `horcrux elect # elect next eligible leader
3131
horcrux elect 2 # elect specific leader`,
3232
SilenceUsage: true,
33-
RunE: func(cmd *cobra.Command, args []string) (err error) {
33+
RunE: func(cmd *cobra.Command, _ []string) (err error) {
3434
if config.Config.ThresholdModeConfig == nil {
3535
return fmt.Errorf("threshold mode configuration is not present in config file")
3636
}
@@ -97,7 +97,7 @@ func getLeaderCmd() *cobra.Command {
9797
Args: cobra.NoArgs,
9898
Example: `horcrux leader`,
9999
SilenceUsage: true,
100-
RunE: func(cmd *cobra.Command, args []string) (err error) {
100+
RunE: func(cmd *cobra.Command, _ []string) (err error) {
101101
thresholdCfg := config.Config.ThresholdModeConfig
102102
if thresholdCfg == nil {
103103
return fmt.Errorf("threshold mode configuration is not present in config file")

cmd/horcrux/cmd/shards.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func createCosignerEd25519ShardsCmd() *cobra.Command {
6666
Use: "create-ed25519-shards",
6767
Args: cobra.NoArgs,
6868
Short: "Create cosigner Ed25519 shards",
69-
RunE: func(cmd *cobra.Command, args []string) (err error) {
69+
RunE: func(cmd *cobra.Command, _ []string) (err error) {
7070
flags := cmd.Flags()
7171

7272
chainID, _ := flags.GetString(flagChainID)
@@ -163,7 +163,7 @@ func createCosignerECIESShardsCmd() *cobra.Command {
163163
Args: cobra.NoArgs,
164164
Short: "Create cosigner ECIES shards",
165165

166-
RunE: func(cmd *cobra.Command, args []string) (err error) {
166+
RunE: func(cmd *cobra.Command, _ []string) (err error) {
167167
shards, _ := cmd.Flags().GetUint8(flagShards)
168168

169169
if shards <= 0 {
@@ -211,7 +211,7 @@ func createCosignerRSAShardsCmd() *cobra.Command {
211211
Args: cobra.NoArgs,
212212
Short: "Create cosigner RSA shards",
213213

214-
RunE: func(cmd *cobra.Command, args []string) (err error) {
214+
RunE: func(cmd *cobra.Command, _ []string) (err error) {
215215
shards, _ := cmd.Flags().GetUint8(flagShards)
216216

217217
if shards <= 0 {

cmd/horcrux/cmd/start.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func startCmd() *cobra.Command {
1616
Short: "Start horcrux signer process",
1717
Args: cobra.NoArgs,
1818
SilenceUsage: true,
19-
RunE: func(cmd *cobra.Command, args []string) error {
19+
RunE: func(cmd *cobra.Command, _ []string) error {
2020
out := cmd.OutOrStdout()
2121
logger := cometlog.NewTMLogger(cometlog.NewSyncWriter(out))
2222

cmd/horcrux/cmd/version.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func versionCmd() *cobra.Command {
6868
Use: "version",
6969
Short: "Version information for horcrux",
7070
SilenceUsage: true,
71-
RunE: func(cmd *cobra.Command, args []string) error {
71+
RunE: func(cmd *cobra.Command, _ []string) error {
7272
bz, err := json.MarshalIndent(NewInfo(), "", " ")
7373
if err != nil {
7474
return err

docker/horcrux/Dockerfile

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS build-env
22

3-
RUN apk add --update --no-cache curl make git libc-dev bash gcc linux-headers eudev-dev
3+
RUN apk add --update --no-cache curl make git libc-dev bash gcc linux-headers eudev-dev wget
44

55
ARG TARGETARCH
66
ARG BUILDARCH
77

88
RUN if [ "${TARGETARCH}" = "arm64" ] && [ "${BUILDARCH}" != "arm64" ]; then \
9-
wget -c https://musl.cc/aarch64-linux-musl-cross.tgz -O - | tar -xzvv --strip-components 1 -C /usr; \
9+
for i in 1 2 3; do \
10+
wget -c https://musl.cc/aarch64-linux-musl-cross.tgz -O - | tar -xzvv --strip-components 1 -C /usr && break || sleep 5; \
11+
done; \
1012
elif [ "${TARGETARCH}" = "amd64" ] && [ "${BUILDARCH}" != "amd64" ]; then \
11-
wget -c https://musl.cc/x86_64-linux-musl-cross.tgz -O - | tar -xzvv --strip-components 1 -C /usr; \
13+
for i in 1 2 3; do \
14+
wget -c https://musl.cc/x86_64-linux-musl-cross.tgz -O - | tar -xzvv --strip-components 1 -C /usr && break || sleep 5; \
15+
done; \
1216
fi
1317

1418
WORKDIR /horcrux
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
FROM --platform=$BUILDPLATFORM golang:1.21-alpine AS build-env
2+
3+
# Install cross-compilation tools from Alpine packages instead of musl.cc
4+
RUN apk add --update --no-cache curl make git libc-dev bash gcc linux-headers eudev-dev
5+
6+
ARG TARGETARCH
7+
ARG BUILDARCH
8+
9+
# Use Alpine's cross-compilation packages instead of downloading from musl.cc
10+
RUN if [ "${TARGETARCH}" = "arm64" ] && [ "${BUILDARCH}" != "arm64" ]; then \
11+
apk add --no-cache gcc-aarch64-linux-gnu musl-dev; \
12+
elif [ "${TARGETARCH}" = "amd64" ] && [ "${BUILDARCH}" != "amd64" ]; then \
13+
apk add --no-cache gcc-x86_64-linux-gnu musl-dev; \
14+
fi
15+
16+
WORKDIR /horcrux
17+
18+
ADD . .
19+
20+
RUN if [ "${TARGETARCH}" = "arm64" ] && [ "${BUILDARCH}" != "arm64" ]; then \
21+
export CC=aarch64-linux-gnu-gcc CXX=aarch64-linux-gnu-g++;\
22+
elif [ "${TARGETARCH}" = "amd64" ] && [ "${BUILDARCH}" != "amd64" ]; then \
23+
export CC=x86_64-linux-gnu-gcc CXX=x86_64-linux-gnu-g++; \
24+
fi; \
25+
GOOS=linux GOARCH=$TARGETARCH CGO_ENABLED=1 LDFLAGS='-linkmode external -extldflags "-static"' make install
26+
27+
RUN if [ -d "/go/bin/linux_${TARGETARCH}" ]; then mv /go/bin/linux_${TARGETARCH}/* /go/bin/; fi
28+
29+
# Use minimal busybox from infra-toolkit image for final scratch image
30+
FROM ghcr.io/strangelove-ventures/infra-toolkit:v0.0.6 AS busybox-min
31+
RUN addgroup --gid 2345 -S horcrux && adduser --uid 2345 -S horcrux -G horcrux
32+
33+
# Use ln and rm from full featured busybox for assembling final image
34+
FROM busybox:1.34.1-musl AS busybox-full
35+
36+
# Build final image from scratch
37+
FROM scratch
38+
39+
LABEL org.opencontainers.image.source="https://github.com/cosmos/horcrux"
40+
41+
WORKDIR /bin
42+
43+
# Install ln (for making hard links) and rm (for cleanup) from full busybox image (will be deleted, only needed for image assembly)
44+
COPY --from=busybox-full /bin/ln /bin/rm ./
45+
46+
# Install minimal busybox image as shell binary (will create hardlinks for the rest of the binaries to this data)
47+
COPY --from=busybox-min /busybox/busybox /bin/sh
48+
49+
# Add hard links for read-only utils, then remove ln and rm
50+
# Will then only have one copy of the busybox minimal binary file with all utils pointing to the same underlying inode
51+
RUN ln sh pwd && \
52+
ln sh ls && \
53+
ln sh cat && \
54+
ln sh less && \
55+
ln sh grep && \
56+
ln sh sleep && \
57+
ln sh env && \
58+
ln sh tar && \
59+
ln sh tee && \
60+
ln sh du && \
61+
rm ln rm
62+
63+
# Install chain binaries
64+
COPY --from=build-env /go/bin/horcrux /bin
65+
66+
# Install trusted CA certificates
67+
COPY --from=busybox-min /etc/ssl/cert.pem /etc/ssl/cert.pem
68+
69+
# Install horcrux user
70+
COPY --from=busybox-min /etc/passwd /etc/passwd
71+
COPY --from=busybox-min --chown=2345:2345 /home/horcrux /home/horcrux
72+
73+
WORKDIR /home/horcrux
74+
USER horcrux

signer/consensus_lock_e2e_test.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
package signer
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/cometbft/cometbft/libs/protoio"
8+
cometproto "github.com/cometbft/cometbft/proto/tendermint/types"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// createTestSignBytes creates proper Tendermint sign bytes for testing
13+
func createTestSignBytesE2E(blockHash []byte, step int8) []byte {
14+
switch step {
15+
case stepPropose:
16+
// Create a CanonicalProposal
17+
proposal := &cometproto.CanonicalProposal{
18+
Type: cometproto.ProposalType,
19+
Height: 100,
20+
Round: 5,
21+
BlockID: &cometproto.CanonicalBlockID{
22+
Hash: blockHash,
23+
},
24+
}
25+
signBytes, _ := protoio.MarshalDelimited(proposal)
26+
return signBytes
27+
28+
case stepPrevote, stepPrecommit:
29+
// Create a CanonicalVote
30+
vote := &cometproto.CanonicalVote{
31+
Type: cometproto.SignedMsgType(step),
32+
Height: 100,
33+
Round: 5,
34+
BlockID: &cometproto.CanonicalBlockID{
35+
Hash: blockHash,
36+
},
37+
}
38+
signBytes, _ := protoio.MarshalDelimited(vote)
39+
return signBytes
40+
41+
default:
42+
return nil
43+
}
44+
}
45+
46+
// TestConsensusLockE2E tests the consensus lock functionality end-to-end
47+
// This test simulates a real scenario where a validator tries to sign conflicting blocks
48+
func TestConsensusLockE2E(t *testing.T) {
49+
// Create a sign state that simulates a validator that has already signed a PRECOMMIT
50+
// for a specific block at height 100, round 5
51+
lockedBlockHash := []byte("locked_block_hash_123456789012345678901234567890")[:32] // Ensure exactly 32 bytes
52+
signState := &SignState{
53+
Height: 100,
54+
Round: 5,
55+
Step: stepPrecommit,
56+
ConsensusLock: ConsensusLock{
57+
Height: 100,
58+
Round: 5,
59+
Value: lockedBlockHash,
60+
},
61+
}
62+
63+
// Test 1: Validator tries to sign a PROPOSAL for the same block in a later round
64+
// This should be allowed (same value)
65+
sameBlockProposal := createTestSignBytesE2E(lockedBlockHash, stepPropose)
66+
err := signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPropose}, sameBlockProposal)
67+
require.NoError(t, err, "Should allow PROPOSAL for same block in later round")
68+
69+
// Test 2: Validator tries to sign a PREVOTE for the same block in a later round
70+
// This should be allowed (same value)
71+
sameBlockPrevote := createTestSignBytesE2E(lockedBlockHash, stepPrevote)
72+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPrevote}, sameBlockPrevote)
73+
require.NoError(t, err, "Should allow PREVOTE for same block in later round")
74+
75+
// Test 3: Validator tries to sign a PROPOSAL for a different block in a later round
76+
// This should be blocked (different value)
77+
differentBlockHash := []byte("different_block_hash_123456789012345678901234567890")[:32]
78+
differentBlockProposal := createTestSignBytesE2E(differentBlockHash, stepPropose)
79+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPropose}, differentBlockProposal)
80+
require.Error(t, err, "Should block PROPOSAL for different block in later round")
81+
require.True(t, IsConsensusLockViolationError(err), "Should be a consensus lock violation error")
82+
83+
// Test 4: Validator tries to sign a PREVOTE for a different block in a later round
84+
// This should be blocked (different value)
85+
differentBlockPrevote := createTestSignBytesE2E(differentBlockHash, stepPrevote)
86+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPrevote}, differentBlockPrevote)
87+
require.Error(t, err, "Should block PREVOTE for different block in later round")
88+
require.True(t, IsConsensusLockViolationError(err), "Should be a consensus lock violation error")
89+
90+
// Test 5: Validator tries to sign a PRECOMMIT for a different block in a later round
91+
// This should be allowed (PRECOMMIT releases the lock)
92+
differentBlockPrecommit := createTestSignBytesE2E(differentBlockHash, stepPrecommit)
93+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPrecommit}, differentBlockPrecommit)
94+
require.NoError(t, err, "Should allow PRECOMMIT for different block in later round (releases lock)")
95+
96+
// Test 6: After signing a PRECOMMIT for a different block, the lock should be updated
97+
// Simulate the lock update
98+
newLock := updateConsensusLock(signState.ConsensusLock, HRSKey{Height: 100, Round: 6, Step: stepPrecommit}, differentBlockPrecommit)
99+
require.True(t, newLock.IsLocked(), "New lock should be active")
100+
require.Equal(t, int64(100), newLock.Height, "Lock should be at height 100")
101+
require.Equal(t, int64(6), newLock.Round, "Lock should be at round 6")
102+
require.Equal(t, differentBlockHash, newLock.Value, "Lock should be on the new block")
103+
104+
// Test 7: Validator tries to sign for a different height
105+
// This should be allowed (locks are height-specific)
106+
differentHeightBytes := createTestSignBytesE2E(differentBlockHash, stepPropose)
107+
err = signState.ValidateConsensusLock(HRSKey{Height: 101, Round: 1, Step: stepPropose}, differentHeightBytes)
108+
require.NoError(t, err, "Should allow signing for different height")
109+
110+
// Test 8: Validator tries to sign for the same height but earlier round
111+
// This should be allowed (locks only apply to later rounds)
112+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 4, Step: stepPropose}, differentHeightBytes)
113+
require.NoError(t, err, "Should allow signing for earlier round")
114+
}
115+
116+
// TestConsensusLockRealWorldScenario tests a realistic scenario
117+
func TestConsensusLockRealWorldScenario(t *testing.T) {
118+
// Scenario: Validator is locked on block A at height 100, round 5
119+
// Network times out and moves to round 6
120+
// New block B is proposed in round 6
121+
// Validator should be prevented from signing block B until it signs a PRECOMMIT
122+
123+
lockedBlockA := []byte("block_A_hash_123456789012345678901234567890")[:32]
124+
signState := &SignState{
125+
Height: 100,
126+
Round: 5,
127+
Step: stepPrecommit,
128+
ConsensusLock: ConsensusLock{
129+
Height: 100,
130+
Round: 5,
131+
Value: lockedBlockA,
132+
},
133+
}
134+
135+
// Round 6: New block B is proposed
136+
blockB := []byte("block_B_hash_123456789012345678901234567890")[:32]
137+
138+
// Validator should NOT be able to sign PROPOSAL for block B
139+
blockBProposal := createTestSignBytesE2E(blockB, stepPropose)
140+
err := signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPropose}, blockBProposal)
141+
require.Error(t, err, "Should block PROPOSAL for block B")
142+
require.True(t, IsConsensusLockViolationError(err), "Should be a consensus lock violation")
143+
144+
// Validator should NOT be able to sign PREVOTE for block B
145+
blockBPrevote := createTestSignBytesE2E(blockB, stepPrevote)
146+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPrevote}, blockBPrevote)
147+
require.Error(t, err, "Should block PREVOTE for block B")
148+
require.True(t, IsConsensusLockViolationError(err), "Should be a consensus lock violation")
149+
150+
// Validator should be able to sign PRECOMMIT for block B (this releases the lock)
151+
blockBPrecommit := createTestSignBytesE2E(blockB, stepPrecommit)
152+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPrecommit}, blockBPrecommit)
153+
require.NoError(t, err, "Should allow PRECOMMIT for block B (releases lock)")
154+
155+
// After signing PRECOMMIT for block B, validator should be locked on block B
156+
newLock := updateConsensusLock(signState.ConsensusLock, HRSKey{Height: 100, Round: 6, Step: stepPrecommit}, blockBPrecommit)
157+
require.True(t, newLock.IsLocked(), "Should be locked on block B")
158+
require.Equal(t, blockB, newLock.Value, "Lock should be on block B")
159+
require.Equal(t, int64(6), newLock.Round, "Lock should be at round 6")
160+
161+
// Update the signState with the new lock
162+
signState.ConsensusLock = newLock
163+
164+
// Now validator should NOT be able to sign for block A in round 7
165+
blockAProposal := createTestSignBytesE2E(lockedBlockA, stepPropose)
166+
err = signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 7, Step: stepPropose}, blockAProposal)
167+
require.Error(t, err, "Should block PROPOSAL for block A in round 7")
168+
require.True(t, IsConsensusLockViolationError(err), "Should be a consensus lock violation")
169+
}
170+
171+
// TestConsensusLockPerformanceE2E tests that consensus lock validation is fast enough for production use
172+
func TestConsensusLockPerformanceE2E(t *testing.T) {
173+
signState := &SignState{
174+
Height: 100,
175+
Round: 5,
176+
Step: stepPrecommit,
177+
ConsensusLock: ConsensusLock{
178+
Height: 100,
179+
Round: 5,
180+
Value: []byte("locked_block_hash_123456789012345678901234567890")[:32],
181+
},
182+
}
183+
184+
blockHash := []byte("different_block_hash_123456789012345678901234567890")[:32]
185+
blockBytes := createTestSignBytesE2E(blockHash, stepPropose)
186+
187+
// Test that validation is fast (should complete in < 1ms per operation)
188+
start := time.Now()
189+
for i := 0; i < 10000; i++ {
190+
err := signState.ValidateConsensusLock(HRSKey{Height: 100, Round: 6, Step: stepPropose}, blockBytes)
191+
require.Error(t, err) // Should always fail due to lock violation
192+
}
193+
duration := time.Since(start)
194+
195+
// Should complete 10,000 validations in well under 1 second
196+
require.Less(t, duration, time.Second, "Consensus lock validation should be very fast")
197+
198+
avgTimePerOp := duration / 10000
199+
require.Less(t, avgTimePerOp, 100*time.Microsecond, "Average time per operation should be < 100μs")
200+
}

0 commit comments

Comments
 (0)