diff --git a/internal/state/block_seal.go b/internal/state/block_seal.go index c12754b..fe64094 100644 --- a/internal/state/block_seal.go +++ b/internal/state/block_seal.go @@ -63,16 +63,16 @@ func SealBlock( var ( sealSignature crypto.BandersnatchSignature - vrfsSignature crypto.BandersnatchSignature + vrfSignature crypto.BandersnatchSignature ) - sealSignature, vrfsSignature, err = SignBlock(*header, winningTicketOrKey, privateKey, entropy) + sealSignature, vrfSignature, err = SignBlock(*header, winningTicketOrKey, privateKey, entropy) if err != nil { return err } header.BlockSealSignature = sealSignature - header.VRFSignature = vrfsSignature + header.VRFSignature = vrfSignature return nil } @@ -89,7 +89,7 @@ func SignBlock( entropy crypto.Hash, // η′3 ) ( sealSignature crypto.BandersnatchSignature, - vrfsSignature crypto.BandersnatchSignature, + vrfSignature crypto.BandersnatchSignature, err error, ) { switch tok := ticketOrKey.(type) { @@ -113,23 +113,25 @@ func SignBlockWithTicket( entropy crypto.Hash, // η′3 ) ( sealSignature crypto.BandersnatchSignature, - vrfsSignature crypto.BandersnatchSignature, + vrfSignature crypto.BandersnatchSignature, err error, ) { - unsealedHeader, err := encodeUnsealedHeader(header) - if err != nil { - return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err - } // Build the context: XT ⌢ η′3 ++ ir sealContext := buildTicketSealContext(entropy, ticket.EntryIndex) - sealSignature, err = bandersnatch.Sign(privateKey, sealContext, unsealedHeader) + // We need to add the VRF signature to the header before we seal. This seems + // like a circle dependency, but it's actually not. To get around this we + // can sign without the aux data (unsealed header), since the seal output + // hash, Y(Hs) will be the same regardless of aux. We then use this to + // produce the VRF signature and add it to the header before we do the final + // seal. + sealVRFSignature, err := bandersnatch.Sign(privateKey, sealContext, []byte{}) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } - sealOutputHash, err := bandersnatch.OutputHash(sealSignature) + sealOutputHash, err := bandersnatch.OutputHash(sealVRFSignature) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } @@ -142,12 +144,24 @@ func SignBlockWithTicket( return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, ErrBlockSealInvalidAuthor } - vrfsSignature, err = signBlockVRFS(sealOutputHash, privateKey) + vrfSignature, err = signBlockVRFS(sealOutputHash, privateKey) + if err != nil { + return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err + } + + // Final seal including the VRF signature added to the header. + header.VRFSignature = vrfSignature + unsealedHeader, err := encodeUnsealedHeader(header) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } - return sealSignature, vrfsSignature, nil + sealSignature, err = bandersnatch.Sign(privateKey, sealContext, unsealedHeader) + if err != nil { + return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err + } + + return sealSignature, vrfSignature, nil } // Helper to build the ticket sealing context. @@ -169,7 +183,7 @@ func SignBlockWithFallback( entropy crypto.Hash, // // η′3 ) ( sealSignature crypto.BandersnatchSignature, - vrfsSignature crypto.BandersnatchSignature, + vrfSignature crypto.BandersnatchSignature, err error, ) { // Extra safety check. Ha's public key should match the winning public key. @@ -181,31 +195,39 @@ func SignBlockWithFallback( return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, ErrBlockSealInvalidAuthor } - unsealedHeader, err := encodeUnsealedHeader(header) + // Build the context: XF ⌢ η′3 + sealContext := buildTicketFallbackContext(entropy) + + // Get Y(Hs) so we can produce and add the VRF signature to the header + // before we do the final seal. + sealVRFSignature, err := bandersnatch.Sign(privateKey, sealContext, []byte{}) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } - // Build the context: XF ⌢ η′3 - sealContext := buildTicketFallbackContext(entropy) - - sealSignature, err = bandersnatch.Sign(privateKey, sealContext, unsealedHeader) + sealOutputHash, err := bandersnatch.OutputHash(sealVRFSignature) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } - sealOutputHash, err := bandersnatch.OutputHash(sealSignature) + vrfSignature, err = signBlockVRFS(sealOutputHash, privateKey) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } - vrfsSignature, err = signBlockVRFS(sealOutputHash, privateKey) + // Final sealing using the included VRF signature. + header.VRFSignature = vrfSignature + unsealedHeader, err := encodeUnsealedHeader(header) if err != nil { return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err } - return sealSignature, vrfsSignature, nil + sealSignature, err = bandersnatch.Sign(privateKey, sealContext, unsealedHeader) + if err != nil { + return crypto.BandersnatchSignature{}, crypto.BandersnatchSignature{}, err + } + return sealSignature, vrfSignature, nil } // Helper to build the fallback sealing context. @@ -221,7 +243,7 @@ func signBlockVRFS( privateKey crypto.BandersnatchPrivateKey, ) (crypto.BandersnatchSignature, error) { // Construct the message: XE ⌢ Y(Hs) - vrfContext := buildVRFSContext(sealOutputHash) + vrfContext := buildVRFContext(sealOutputHash) // Sign the constructed message to get Hv. vrfSignature, err := bandersnatch.Sign(privateKey, vrfContext, []byte{}) @@ -233,7 +255,7 @@ func signBlockVRFS( } // Helper to build the fallback sealing context. -func buildVRFSContext(sealOutputHash crypto.BandersnatchOutputHash) []byte { +func buildVRFContext(sealOutputHash crypto.BandersnatchOutputHash) []byte { // Construct the message: XE ⌢ Y(Hs) return append([]byte(EntropyContext), sealOutputHash[:]...) } @@ -251,3 +273,109 @@ func encodeUnsealedHeader(header block.Header) ([]byte, error) { // See equation C.19 in the graypaper. (v0.5.4) return bytes[:len(bytes)-96], nil } + +func VerifyBlockSeal( + header *block.Header, + state *State, +) (bool, error) { + winningTicketOrKey, err := getWinningTicketOrKey(header, state) + if err != nil { + return false, err + } + + entropy := state.EntropyPool[3] // η_3 + + return VerifyBlockSignatures(*header, winningTicketOrKey, state.ValidatorState.CurrentValidators, entropy) +} + +func VerifyBlockSignatures( + header block.Header, + ticketOrKey TicketOrKey, + currentValidators safrole.ValidatorsData, + entropy crypto.Hash, +) (bool, error) { + unsealedHeader, err := encodeUnsealedHeader(header) + if err != nil { + return false, err + } + + sealOutputHash, err := bandersnatch.OutputHash(header.BlockSealSignature) + if err != nil { + return false, err + } + + switch tok := ticketOrKey.(type) { + // The ticket case is more challenging because we don't immediately know the + // public key of the validator who signed. + case block.Ticket: + // Sanity check. + if sealOutputHash != tok.Identifier { + return false, nil + } + + sealContext := buildTicketSealContext(entropy, tok.EntryIndex) + publicKey := crypto.BandersnatchPublicKey{} + + // Since the original ticket submission was anonymous we need to figure + // out which validator signed. We do this by looping through all + // currentValidators and seeing which validator's bandersnatch public + // key is able to verify the seal signature. + for _, keys := range currentValidators { + if keys == nil { + continue + } + + ok, _ := bandersnatch.Verify( + keys.Bandersnatch, + sealContext, + unsealedHeader, + header.BlockSealSignature, + ) + if ok { + publicKey = keys.Bandersnatch + break + } + } + + if publicKey == (crypto.BandersnatchPublicKey{}) { + return false, nil + } + + // Use the found public key to also check the VRF signature. + ok, _ := bandersnatch.Verify( + publicKey, + buildVRFContext(sealOutputHash), + []byte{}, + header.VRFSignature, + ) + if !ok { + return false, nil + } + return true, nil + + // This case is much easier since we actually know the public key from the + // start. + case crypto.BandersnatchPublicKey: + ok, _ := bandersnatch.Verify( + tok, + buildTicketFallbackContext(entropy), + unsealedHeader, + header.BlockSealSignature, + ) + if !ok { + return false, nil + } + ok, _ = bandersnatch.Verify( + tok, + buildVRFContext(sealOutputHash), + []byte{}, + header.VRFSignature, + ) + if !ok { + return false, nil + } + return true, nil + default: + return false, fmt.Errorf("unexpected type for ticketOrKey: %T", tok) + } +} diff --git a/internal/state/block_seal_test.go b/internal/state/block_seal_test.go index 3e7688e..bca1859 100644 --- a/internal/state/block_seal_test.go +++ b/internal/state/block_seal_test.go @@ -15,15 +15,14 @@ import ( "golang.org/x/exp/rand" ) -func TestSealBlockTicket(t *testing.T) { +func TestSealVerifyBlockTicket(t *testing.T) { entropy := testutils.RandomHash(t) ticketBodies := randomTicketBodies(t, entropy) randomTimeslot := testutils.RandomUint32() % jamtime.TimeslotsPerEpoch t.Logf("random timeslot: %d", randomTimeslot) - // Replace one of the keys in the accumulator with our public key. This - // should later be selected as the winning key. + // Create a winning ticket for our private key. privateKey := testutils.RandomBandersnatchPrivateKey(t) ticket := createTicket(t, privateKey, entropy, 0) ticketBodies[randomTimeslot] = ticket @@ -38,6 +37,20 @@ func TestSealBlockTicket(t *testing.T) { TimeSlotIndex: jamtime.Timeslot(randomTimeslot), } + currentValidators := safrole.ValidatorsData{} + for i := range currentValidators { + currentValidators[i] = &crypto.ValidatorKey{ + Bandersnatch: testutils.RandomBandersnatchPublicKey(t), + } + } + + // Add our public key to the current validators set. + publicKey, err := bandersnatch.Public(privateKey) + require.NoError(t, err) + currentValidators[1] = &crypto.ValidatorKey{ + Bandersnatch: publicKey, + } + state := &State{ EntropyPool: [4]crypto.Hash{ testutils.RandomHash(t), @@ -49,32 +62,38 @@ func TestSealBlockTicket(t *testing.T) { SafroleState: safrole.State{ SealingKeySeries: ticketAccumulator, }, + CurrentValidators: currentValidators, }, } - err := SealBlock(header, state, privateKey) + err = SealBlock(header, state, privateKey) require.NoError(t, err) assert.NotEmpty(t, header.BlockSealSignature) assert.NotEmpty(t, header.VRFSignature) - // Sanity checks that we did sign. - expectedTicketID, err := bandersnatch.OutputHash(header.BlockSealSignature) + // Tampering with the block seal signature should fail verification. + sealSignature := header.BlockSealSignature + header.BlockSealSignature = testutils.RandomBandersnatchSignature(t) + ok, err := VerifyBlockSeal(header, state) require.NoError(t, err) - // Check that our seal produces the same output hash used by the ticket. - assert.Equal(t, expectedTicketID, ticket.Identifier) - publicKey, err := bandersnatch.Public(privateKey) + require.False(t, ok) + header.BlockSealSignature = sealSignature + + // Tampering with the VRF signature should fail verification. + vrfSignature := header.VRFSignature + header.VRFSignature = testutils.RandomBandersnatchSignature(t) + ok, err = VerifyBlockSeal(header, state) + require.NoError(t, err) + require.False(t, ok) + header.VRFSignature = vrfSignature + + // Valid signatures verify. + ok, err = VerifyBlockSeal(header, state) require.NoError(t, err) - // Check Hv. - ok, _ := bandersnatch.Verify( - publicKey, - buildVRFSContext(expectedTicketID), - []byte{}, - header.VRFSignature, - ) require.True(t, ok) } -func TestSealBlockFallback(t *testing.T) { +func TestSealVerifyBlockFallback(t *testing.T) { privateKey := testutils.RandomBandersnatchPrivateKey(t) publicKey, err := bandersnatch.Public(privateKey) require.NoError(t, err) @@ -97,8 +116,6 @@ func TestSealBlockFallback(t *testing.T) { ExtrinsicHash: testutils.RandomHash(t), TimeSlotIndex: jamtime.Timeslot(randomTimeslot), } - unsealedHeader, err := encodeUnsealedHeader(*header) - require.NoError(t, err) entropy := testutils.RandomHash(t) state := &State{ @@ -120,23 +137,26 @@ func TestSealBlockFallback(t *testing.T) { assert.NotEmpty(t, header.BlockSealSignature) assert.NotEmpty(t, header.VRFSignature) - // Sanity checks that we did sign. - vrfInput := buildTicketFallbackContext(entropy) + // Tampering with the block seal signature should fail verification. + sealSignature := header.BlockSealSignature + header.BlockSealSignature = testutils.RandomBandersnatchSignature(t) + ok, err := VerifyBlockSeal(header, state) require.NoError(t, err) - // Check Hs. - ok, _ := bandersnatch.Verify(publicKey, vrfInput, unsealedHeader, header.BlockSealSignature) - require.True(t, ok) - sealOutputHash, err := bandersnatch.OutputHash(header.BlockSealSignature) - require.NoError(t, err) - // Check Hv. - ok, _ = bandersnatch.Verify( - publicKey, - buildVRFSContext(sealOutputHash), - []byte{}, - header.VRFSignature, - ) - require.True(t, ok) + require.False(t, ok) + header.BlockSealSignature = sealSignature + + // Tampering with the VRF signature should fail verification. + vrfSignature := header.VRFSignature + header.VRFSignature = testutils.RandomBandersnatchSignature(t) + ok, err = VerifyBlockSeal(header, state) + require.NoError(t, err) + require.False(t, ok) + header.VRFSignature = vrfSignature + // Valid signatures verify. + ok, err = VerifyBlockSeal(header, state) + require.NoError(t, err) + require.True(t, ok) } func TestSealBlockInvalidAuthor(t *testing.T) { @@ -172,6 +192,128 @@ func TestSealBlockInvalidAuthor(t *testing.T) { require.ErrorIs(t, err, ErrBlockSealInvalidAuthor) } +func TestVerfyBlockInvalidVRFSignature(t *testing.T) { + entropy := testutils.RandomHash(t) + ticketBodies := randomTicketBodies(t, entropy) + + randomTimeslot := testutils.RandomUint32() % jamtime.TimeslotsPerEpoch + t.Logf("random timeslot: %d", randomTimeslot) + + // Create a winning ticket for our private key. + privateKey := testutils.RandomBandersnatchPrivateKey(t) + ticket := createTicket(t, privateKey, entropy, 0) + ticketBodies[randomTimeslot] = ticket + + ticketAccumulator := safrole.TicketAccumulator{} + ticketAccumulator.Set(ticketBodies) + + header := &block.Header{ + ParentHash: testutils.RandomHash(t), + PriorStateRoot: testutils.RandomHash(t), + ExtrinsicHash: testutils.RandomHash(t), + TimeSlotIndex: jamtime.Timeslot(randomTimeslot), + } + + currentValidators := safrole.ValidatorsData{} + for i := range currentValidators { + currentValidators[i] = &crypto.ValidatorKey{ + Bandersnatch: testutils.RandomBandersnatchPublicKey(t), + } + } + + // Add our public key to the current validators set. + publicKey, err := bandersnatch.Public(privateKey) + require.NoError(t, err) + currentValidators[1] = &crypto.ValidatorKey{ + Bandersnatch: publicKey, + } + + state := &State{ + EntropyPool: [4]crypto.Hash{ + testutils.RandomHash(t), + testutils.RandomHash(t), + testutils.RandomHash(t), + entropy, + }, + ValidatorState: validator.ValidatorState{ + SafroleState: safrole.State{ + SealingKeySeries: ticketAccumulator, + }, + CurrentValidators: currentValidators, + }, + } + + vrfSignature := testutils.RandomBandersnatchSignature(t) + sealContext := buildTicketSealContext(entropy, ticket.EntryIndex) + + header.VRFSignature = vrfSignature + unsealedHeader, err := encodeUnsealedHeader(*header) + require.NoError(t, err) + + sealSignature, err := bandersnatch.Sign(privateKey, sealContext, unsealedHeader) + require.NoError(t, err) + header.BlockSealSignature = sealSignature + + ok, err := VerifyBlockSeal(header, state) + require.NoError(t, err) + require.False(t, ok) +} + +func TestVerifyBlockInvalidVRFFallback(t *testing.T) { + privateKey := testutils.RandomBandersnatchPrivateKey(t) + publicKey, err := bandersnatch.Public(privateKey) + require.NoError(t, err) + + var epochKeys = testutils.RandomEpochKeys(t) + + randomTimeslot := testutils.RandomUint32() % jamtime.TimeslotsPerEpoch + t.Logf("random timeslot: %d", randomTimeslot) + + // Replace one of the keys in the accumulator with our public key. This + // should later be selected as the winning key. + epochKeys[randomTimeslot] = publicKey + + ticketAccumulator := safrole.TicketAccumulator{} + ticketAccumulator.Set(epochKeys) + + header := &block.Header{ + ParentHash: testutils.RandomHash(t), + PriorStateRoot: testutils.RandomHash(t), + ExtrinsicHash: testutils.RandomHash(t), + TimeSlotIndex: jamtime.Timeslot(randomTimeslot), + } + + entropy := testutils.RandomHash(t) + state := &State{ + EntropyPool: [4]crypto.Hash{ + testutils.RandomHash(t), + testutils.RandomHash(t), + testutils.RandomHash(t), + entropy, + }, + ValidatorState: validator.ValidatorState{ + SafroleState: safrole.State{ + SealingKeySeries: ticketAccumulator, + }, + }, + } + + vrfSignature := testutils.RandomBandersnatchSignature(t) + sealContext := buildTicketFallbackContext(entropy) + + header.VRFSignature = vrfSignature + unsealedHeader, err := encodeUnsealedHeader(*header) + require.NoError(t, err) + + sealSignature, err := bandersnatch.Sign(privateKey, sealContext, unsealedHeader) + require.NoError(t, err) + header.BlockSealSignature = sealSignature + + ok, err := VerifyBlockSeal(header, state) + require.NoError(t, err) + require.False(t, ok) +} + func createTicket(t *testing.T, privateKey crypto.BandersnatchPrivateKey, entropy crypto.Hash, attempt uint8) block.Ticket { vrfInput := buildTicketSealContext(entropy, attempt) signature, err := bandersnatch.Sign(privateKey, vrfInput, []byte{}) diff --git a/internal/testutils/utils.go b/internal/testutils/utils.go index 4bbb900..c699648 100644 --- a/internal/testutils/utils.go +++ b/internal/testutils/utils.go @@ -57,10 +57,10 @@ func RandomED25519PublicKey(t *testing.T) ed25519.PublicKey { } func RandomBandersnatchPublicKey(t *testing.T) crypto.BandersnatchPublicKey { - hash := make([]byte, crypto.BandersnatchSerializedSize) - _, err := rand.Read(hash) + privateKey := RandomBandersnatchPrivateKey(t) + publicKey, err := bandersnatch.Public(privateKey) require.NoError(t, err) - return crypto.BandersnatchPublicKey(hash) + return publicKey } func RandomBandersnatchPrivateKey(t *testing.T) crypto.BandersnatchPrivateKey { @@ -96,10 +96,13 @@ func RandomValidatorKey(t *testing.T) *crypto.ValidatorKey { } func RandomBandersnatchSignature(t *testing.T) crypto.BandersnatchSignature { + privateKey := RandomBandersnatchPrivateKey(t) hash := make([]byte, 96) _, err := rand.Read(hash) require.NoError(t, err) - return crypto.BandersnatchSignature(hash) + signature, err := bandersnatch.Sign(privateKey, hash, hash) + require.NoError(t, err) + return signature } func RandomBandersnatchRingCommitment(t *testing.T) crypto.RingCommitment { diff --git a/tests/integration/sealing_test.go b/tests/integration/sealing_test.go new file mode 100644 index 0000000..6cf011d --- /dev/null +++ b/tests/integration/sealing_test.go @@ -0,0 +1,85 @@ +//go:build integration + +package integration_test + +import ( + "encoding/hex" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/eigerco/strawberry/internal/block" + "github.com/eigerco/strawberry/internal/crypto" + "github.com/eigerco/strawberry/internal/state" + "github.com/eigerco/strawberry/internal/testutils" + "github.com/eigerco/strawberry/pkg/serialization/codec/jam" + "github.com/stretchr/testify/require" +) + +func TestBlockSealCommunityVectors(t *testing.T) { + testFiles := []string{ + "vectors_community/sealing/0-0.json", + "vectors_community/sealing/0-1.json", + "vectors_community/sealing/0-2.json", + "vectors_community/sealing/0-4.json", + "vectors_community/sealing/0-5.json", + "vectors_community/sealing/1-0.json", + "vectors_community/sealing/1-1.json", + "vectors_community/sealing/1-2.json", + "vectors_community/sealing/1-3.json", + "vectors_community/sealing/1-4.json", + "vectors_community/sealing/1-5.json", + } + + for _, tf := range testFiles { + t.Run(filepath.Base(tf), func(t *testing.T) { + file, err := os.ReadFile(tf) + require.NoError(t, err) + + var tv blockSealTestData + err = json.Unmarshal(file, &tv) + require.NoError(t, err) + + var header block.Header + headerBytes := testutils.MustFromHex(t, tv.HeaderBytes) + err = jam.Unmarshal(headerBytes, &header) + require.NoError(t, err) + + privateKey := crypto.BandersnatchPrivateKey(testutils.MustFromHex(t, tv.BandersnatchPriv)) + entropy := crypto.Hash(testutils.MustFromHex(t, tv.Eta3)) + + var ticketOrKey state.TicketOrKey + if tv.T == 1 { + ticketOrKey = block.Ticket{ + Identifier: crypto.BandersnatchOutputHash(testutils.MustFromHex(t, tv.TicketID)), + EntryIndex: tv.Attempt, + } + } else { // Fallback case. + ticketOrKey = crypto.BandersnatchPublicKey(testutils.MustFromHex(t, tv.BandersnatchPub)) + } + + sealSignature, vrfsSignature, err := state.SignBlock(header, ticketOrKey, privateKey, entropy) + require.NoError(t, err) + + require.Equal(t, hex.EncodeToString(sealSignature[:]), tv.Hs) + require.Equal(t, hex.EncodeToString(vrfsSignature[:]), tv.Hv) + }) + } +} + +type blockSealTestData struct { + BandersnatchPub string `json:"bandersnatch_pub"` + BandersnatchPriv string `json:"bandersnatch_priv"` + TicketID string `json:"ticket_id"` + Attempt uint8 `json:"attempt"` + CForHs string `json:"c_for_H_s"` + MForHs string `json:"m_for_H_s"` + Hs string `json:"H_s"` + CForHv string `json:"c_for_H_v"` + MForHv string `json:"m_for_H_v"` + Hv string `json:"H_v"` + Eta3 string `json:"eta3"` + T int `json:"T"` + HeaderBytes string `json:"header_bytes"` +}