@@ -706,6 +706,129 @@ func TestQBFTConsensusHandle(t *testing.T) {
706706 }
707707}
708708
709+ // TestQBFTConsensusHandleAmplificationLimits verifies that handle rejects messages
710+ // carrying more justifications or values than a legitimate consensus message ever
711+ // needs, before doing the expensive per-element signature recovery / unmarshalling.
712+ // This caps the CPU/memory amplification a single authenticated peer can inflict.
713+ func TestQBFTConsensusHandleAmplificationLimits (t * testing.T ) {
714+ // newConsensus returns a single-node consensus, so max justifications = 2*nodes = 2.
715+ newConsensus := func (t * testing.T ) (* Consensus , * k1.PrivateKey ) {
716+ t .Helper ()
717+
718+ var c Consensus
719+
720+ deadliner := coremocks .NewDeadliner (t )
721+ deadliner .On ("Add" , mock .Anything ).Maybe ().Return (core .DeadlineScheduled )
722+ c .deadliner = deadliner
723+ c .gaterFunc = func (core.Duty ) bool { return true }
724+ c .mutable .instances = make (map [core.Duty ]* instance.IO [Msg ])
725+
726+ p2pKey := testutil .GenerateInsecureK1Key (t , 0 )
727+ c .pubkeys = make (map [int64 ]* k1.PublicKey )
728+ c .pubkeys [0 ] = p2pKey .PubKey ()
729+
730+ return & c , p2pKey
731+ }
732+
733+ // signedBase returns a validly-signed main message so verification reaches the limit checks.
734+ signedBase := func (t * testing.T , p2pKey * k1.PrivateKey ) * pbv1.QBFTConsensusMsg {
735+ t .Helper ()
736+
737+ base := & pbv1.QBFTConsensusMsg {Msg : newRandomQBFTMsg (t )}
738+ base .Msg .PeerIdx = 0
739+ base .Msg .Round = 1
740+ base .Msg .Duty = & pbv1.Duty {Slot : 42 , Type : 1 }
741+
742+ msgHash , err := hashProto (base .GetMsg ())
743+ require .NoError (t , err )
744+
745+ sign , err := k1util .Sign (p2pKey , msgHash [:])
746+ require .NoError (t , err )
747+
748+ base .Msg .Signature = sign
749+
750+ return base
751+ }
752+
753+ // signedJustification returns a validly-signed justification matching the base message's duty.
754+ signedJustification := func (t * testing.T , p2pKey * k1.PrivateKey ) * pbv1.QBFTMsg {
755+ t .Helper ()
756+
757+ j := newRandomQBFTMsg (t )
758+ j .PeerIdx = 0
759+ j .Round = 1 // verifyMsg requires round > 0, don't rely on the random value.
760+ j .Duty = & pbv1.Duty {Slot : 42 , Type : 1 }
761+
762+ jHash , err := hashProto (j )
763+ require .NoError (t , err )
764+
765+ j .Signature , err = k1util .Sign (p2pKey , jHash [:])
766+ require .NoError (t , err )
767+
768+ return j
769+ }
770+
771+ t .Run ("too many justifications rejected" , func (t * testing.T ) {
772+ c , p2pKey := newConsensus (t )
773+ base := signedBase (t , p2pKey )
774+
775+ // 3 justifications > 2*nodes (2). Content is irrelevant since the count
776+ // check runs before any per-justification verification.
777+ for range 3 {
778+ base .Justification = append (base .Justification , & pbv1.QBFTMsg {})
779+ }
780+
781+ _ , _ , err := c .handle (context .Background (), "peerID" , base )
782+ require .ErrorContains (t , err , "too many justifications" )
783+ })
784+
785+ t .Run ("max justifications accepted" , func (t * testing.T ) {
786+ c , p2pKey := newConsensus (t )
787+ base := signedBase (t , p2pKey )
788+
789+ // Exactly 2*nodes (2) justifications must not be rejected by the count check.
790+ for range 2 {
791+ base .Justification = append (base .Justification , signedJustification (t , p2pKey ))
792+ }
793+
794+ _ , _ , err := c .handle (context .Background (), "peerID" , base )
795+ require .NoError (t , err )
796+ })
797+
798+ t .Run ("too many values rejected" , func (t * testing.T ) {
799+ c , p2pKey := newConsensus (t )
800+ base := signedBase (t , p2pKey )
801+
802+ // 0 justifications => max values = 2*(0+1) = 2. Provide 3.
803+ base .Values = []* anypb.Any {{}, {}, {}}
804+
805+ _ , _ , err := c .handle (context .Background (), "peerID" , base )
806+ require .ErrorContains (t , err , "too many values" )
807+ })
808+
809+ t .Run ("max values accepted" , func (t * testing.T ) {
810+ c , p2pKey := newConsensus (t )
811+ base := signedBase (t , p2pKey )
812+
813+ // 2 justifications => max values = 2*(2+1) = 6. A message carrying exactly
814+ // the maximum must pass the count check and the rest of handle, guarding
815+ // against the bound being tightened below the legitimate maximum.
816+ for range 2 {
817+ base .Justification = append (base .Justification , signedJustification (t , p2pKey ))
818+ }
819+
820+ for i := range 6 {
821+ value , err := anypb .New (& pbv1.Duty {Slot : uint64 (i + 1 )})
822+ require .NoError (t , err )
823+
824+ base .Values = append (base .Values , value )
825+ }
826+
827+ _ , _ , err := c .handle (context .Background (), "peerID" , base )
828+ require .NoError (t , err )
829+ })
830+ }
831+
709832func TestInstanceIO_MaybeStart (t * testing.T ) {
710833 t .Run ("MaybeStart for new instance" , func (t * testing.T ) {
711834 inst1 := instance .NewIO [Msg ]()
0 commit comments