From 674915ee097a06a686f1ce7e05e65141c09b9d6e Mon Sep 17 00:00:00 2001 From: John Jannotti Date: Thu, 21 Nov 2024 12:56:18 -0500 Subject: [PATCH] Introduce HeartbeatProof The HeartbeatProof type just makes the Heartbeat transactions a little cleaner - they lack the legacy field that OneTimeSignature can't remove. --- crypto/msgp_gen.go | 236 +++++++++++++++++++++++++++++++++ crypto/msgp_gen_test.go | 60 +++++++++ crypto/onetimesig.go | 39 ++++++ data/transactions/heartbeat.go | 2 +- data/transactions/msgp_gen.go | 4 +- data/txntest/txn.go | 2 +- ledger/apply/heartbeat.go | 2 +- ledger/apply/heartbeat_test.go | 4 +- 8 files changed, 342 insertions(+), 7 deletions(-) diff --git a/crypto/msgp_gen.go b/crypto/msgp_gen.go index ab5bdceb88..fc279029a0 100644 --- a/crypto/msgp_gen.go +++ b/crypto/msgp_gen.go @@ -111,6 +111,16 @@ import ( // |-----> MsgIsZero // |-----> HashTypeMaxSize() // +// HeartbeatProof +// |-----> (*) MarshalMsg +// |-----> (*) CanMarshalMsg +// |-----> (*) UnmarshalMsg +// |-----> (*) UnmarshalMsgWithState +// |-----> (*) CanUnmarshalMsg +// |-----> (*) Msgsize +// |-----> (*) MsgIsZero +// |-----> HeartbeatProofMaxSize() +// // MasterDerivationKey // |-----> (*) MarshalMsg // |-----> (*) CanMarshalMsg @@ -1169,6 +1179,232 @@ func HashTypeMaxSize() (s int) { return } +// MarshalMsg implements msgp.Marshaler +func (z *HeartbeatProof) MarshalMsg(b []byte) (o []byte) { + o = msgp.Require(b, z.Msgsize()) + // omitempty: check for empty values + zb0006Len := uint32(5) + var zb0006Mask uint8 /* 6 bits */ + if (*z).PK == (ed25519PublicKey{}) { + zb0006Len-- + zb0006Mask |= 0x2 + } + if (*z).PK1Sig == (ed25519Signature{}) { + zb0006Len-- + zb0006Mask |= 0x4 + } + if (*z).PK2 == (ed25519PublicKey{}) { + zb0006Len-- + zb0006Mask |= 0x8 + } + if (*z).PK2Sig == (ed25519Signature{}) { + zb0006Len-- + zb0006Mask |= 0x10 + } + if (*z).Sig == (ed25519Signature{}) { + zb0006Len-- + zb0006Mask |= 0x20 + } + // variable map header, size zb0006Len + o = append(o, 0x80|uint8(zb0006Len)) + if zb0006Len != 0 { + if (zb0006Mask & 0x2) == 0 { // if not empty + // string "p" + o = append(o, 0xa1, 0x70) + o = msgp.AppendBytes(o, ((*z).PK)[:]) + } + if (zb0006Mask & 0x4) == 0 { // if not empty + // string "p1s" + o = append(o, 0xa3, 0x70, 0x31, 0x73) + o = msgp.AppendBytes(o, ((*z).PK1Sig)[:]) + } + if (zb0006Mask & 0x8) == 0 { // if not empty + // string "p2" + o = append(o, 0xa2, 0x70, 0x32) + o = msgp.AppendBytes(o, ((*z).PK2)[:]) + } + if (zb0006Mask & 0x10) == 0 { // if not empty + // string "p2s" + o = append(o, 0xa3, 0x70, 0x32, 0x73) + o = msgp.AppendBytes(o, ((*z).PK2Sig)[:]) + } + if (zb0006Mask & 0x20) == 0 { // if not empty + // string "s" + o = append(o, 0xa1, 0x73) + o = msgp.AppendBytes(o, ((*z).Sig)[:]) + } + } + return +} + +func (_ *HeartbeatProof) CanMarshalMsg(z interface{}) bool { + _, ok := (z).(*HeartbeatProof) + return ok +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *HeartbeatProof) UnmarshalMsgWithState(bts []byte, st msgp.UnmarshalState) (o []byte, err error) { + if st.AllowableDepth == 0 { + err = msgp.ErrMaxDepthExceeded{} + return + } + st.AllowableDepth-- + var field []byte + _ = field + var zb0006 int + var zb0007 bool + zb0006, zb0007, bts, err = msgp.ReadMapHeaderBytes(bts) + if _, ok := err.(msgp.TypeError); ok { + zb0006, zb0007, bts, err = msgp.ReadArrayHeaderBytes(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0006 > 0 { + zb0006-- + bts, err = msgp.ReadExactBytes(bts, ((*z).Sig)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "Sig") + return + } + } + if zb0006 > 0 { + zb0006-- + bts, err = msgp.ReadExactBytes(bts, ((*z).PK)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "PK") + return + } + } + if zb0006 > 0 { + zb0006-- + bts, err = msgp.ReadExactBytes(bts, ((*z).PK2)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "PK2") + return + } + } + if zb0006 > 0 { + zb0006-- + bts, err = msgp.ReadExactBytes(bts, ((*z).PK1Sig)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "PK1Sig") + return + } + } + if zb0006 > 0 { + zb0006-- + bts, err = msgp.ReadExactBytes(bts, ((*z).PK2Sig)[:]) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "PK2Sig") + return + } + } + if zb0006 > 0 { + err = msgp.ErrTooManyArrayFields(zb0006) + if err != nil { + err = msgp.WrapError(err, "struct-from-array") + return + } + } + } else { + if err != nil { + err = msgp.WrapError(err) + return + } + if zb0007 { + (*z) = HeartbeatProof{} + } + for zb0006 > 0 { + zb0006-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + err = msgp.WrapError(err) + return + } + switch string(field) { + case "s": + bts, err = msgp.ReadExactBytes(bts, ((*z).Sig)[:]) + if err != nil { + err = msgp.WrapError(err, "Sig") + return + } + case "p": + bts, err = msgp.ReadExactBytes(bts, ((*z).PK)[:]) + if err != nil { + err = msgp.WrapError(err, "PK") + return + } + case "p2": + bts, err = msgp.ReadExactBytes(bts, ((*z).PK2)[:]) + if err != nil { + err = msgp.WrapError(err, "PK2") + return + } + case "p1s": + bts, err = msgp.ReadExactBytes(bts, ((*z).PK1Sig)[:]) + if err != nil { + err = msgp.WrapError(err, "PK1Sig") + return + } + case "p2s": + bts, err = msgp.ReadExactBytes(bts, ((*z).PK2Sig)[:]) + if err != nil { + err = msgp.WrapError(err, "PK2Sig") + return + } + default: + err = msgp.ErrNoField(string(field)) + if err != nil { + err = msgp.WrapError(err) + return + } + } + } + } + o = bts + return +} + +func (z *HeartbeatProof) UnmarshalMsg(bts []byte) (o []byte, err error) { + return z.UnmarshalMsgWithState(bts, msgp.DefaultUnmarshalState) +} +func (_ *HeartbeatProof) CanUnmarshalMsg(z interface{}) bool { + _, ok := (z).(*HeartbeatProof) + return ok +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z *HeartbeatProof) Msgsize() (s int) { + s = 1 + 2 + msgp.ArrayHeaderSize + (64 * (msgp.ByteSize)) + 2 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 3 + msgp.ArrayHeaderSize + (32 * (msgp.ByteSize)) + 4 + msgp.ArrayHeaderSize + (64 * (msgp.ByteSize)) + 4 + msgp.ArrayHeaderSize + (64 * (msgp.ByteSize)) + return +} + +// MsgIsZero returns whether this is a zero value +func (z *HeartbeatProof) MsgIsZero() bool { + return ((*z).Sig == (ed25519Signature{})) && ((*z).PK == (ed25519PublicKey{})) && ((*z).PK2 == (ed25519PublicKey{})) && ((*z).PK1Sig == (ed25519Signature{})) && ((*z).PK2Sig == (ed25519Signature{})) +} + +// MaxSize returns a maximum valid message size for this message type +func HeartbeatProofMaxSize() (s int) { + s = 1 + 2 + // Calculating size of array: z.Sig + s += msgp.ArrayHeaderSize + ((64) * (msgp.ByteSize)) + s += 2 + // Calculating size of array: z.PK + s += msgp.ArrayHeaderSize + ((32) * (msgp.ByteSize)) + s += 3 + // Calculating size of array: z.PK2 + s += msgp.ArrayHeaderSize + ((32) * (msgp.ByteSize)) + s += 4 + // Calculating size of array: z.PK1Sig + s += msgp.ArrayHeaderSize + ((64) * (msgp.ByteSize)) + s += 4 + // Calculating size of array: z.PK2Sig + s += msgp.ArrayHeaderSize + ((64) * (msgp.ByteSize)) + return +} + // MarshalMsg implements msgp.Marshaler func (z *MasterDerivationKey) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) diff --git a/crypto/msgp_gen_test.go b/crypto/msgp_gen_test.go index b3fb95150b..0105a58f1d 100644 --- a/crypto/msgp_gen_test.go +++ b/crypto/msgp_gen_test.go @@ -434,6 +434,66 @@ func BenchmarkUnmarshalHashFactory(b *testing.B) { } } +func TestMarshalUnmarshalHeartbeatProof(t *testing.T) { + partitiontest.PartitionTest(t) + v := HeartbeatProof{} + bts := v.MarshalMsg(nil) + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func TestRandomizedEncodingHeartbeatProof(t *testing.T) { + protocol.RunEncodingTest(t, &HeartbeatProof{}) +} + +func BenchmarkMarshalMsgHeartbeatProof(b *testing.B) { + v := HeartbeatProof{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgHeartbeatProof(b *testing.B) { + v := HeartbeatProof{} + bts := make([]byte, 0, v.Msgsize()) + bts = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalHeartbeatProof(b *testing.B) { + v := HeartbeatProof{} + bts := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + func TestMarshalUnmarshalMasterDerivationKey(t *testing.T) { partitiontest.PartitionTest(t) v := MasterDerivationKey{} diff --git a/crypto/onetimesig.go b/crypto/onetimesig.go index d05ccaa961..a817e590fe 100644 --- a/crypto/onetimesig.go +++ b/crypto/onetimesig.go @@ -57,6 +57,45 @@ type OneTimeSignature struct { PK2Sig ed25519Signature `codec:"p2s"` } +// A HeartbeatProof is functionally equivalent to a OneTimeSignature, but it has +// been cleaned up for use as a transaction field in heartbeat transactions. +type HeartbeatProof struct { + _struct struct{} `codec:",omitempty,omitemptyarray"` + + // Sig is a signature of msg under the key PK. + Sig ed25519Signature `codec:"s"` + PK ed25519PublicKey `codec:"p"` + + // PK2 is used to verify a two-level ephemeral signature. + PK2 ed25519PublicKey `codec:"p2"` + // PK1Sig is a signature of OneTimeSignatureSubkeyOffsetID(PK, Batch, Offset) under the key PK2. + PK1Sig ed25519Signature `codec:"p1s"` + // PK2Sig is a signature of OneTimeSignatureSubkeyBatchID(PK2, Batch) under the master key (OneTimeSignatureVerifier). + PK2Sig ed25519Signature `codec:"p2s"` +} + +// ToOneTimeSignature converts a HeartbeatProof to a OneTimeSignature. +func (hbp HeartbeatProof) ToOneTimeSignature() OneTimeSignature { + return OneTimeSignature{ + Sig: hbp.Sig, + PK: hbp.PK, + PK2: hbp.PK2, + PK1Sig: hbp.PK1Sig, + PK2Sig: hbp.PK2Sig, + } +} + +// ToHeartbeatProof converts a OneTimeSignature to a HeartbeatProof. +func (ots OneTimeSignature) ToHeartbeatProof() HeartbeatProof { + return HeartbeatProof{ + Sig: ots.Sig, + PK: ots.PK, + PK2: ots.PK2, + PK1Sig: ots.PK1Sig, + PK2Sig: ots.PK2Sig, + } +} + // A OneTimeSignatureSubkeyBatchID identifies an ephemeralSubkey of a batch // for the purposes of signing it with the top-level master key. type OneTimeSignatureSubkeyBatchID struct { diff --git a/data/transactions/heartbeat.go b/data/transactions/heartbeat.go index 873a905079..48a4df6c69 100644 --- a/data/transactions/heartbeat.go +++ b/data/transactions/heartbeat.go @@ -32,7 +32,7 @@ type HeartbeatTxnFields struct { HbAddress basics.Address `codec:"hbad"` // HbProof is a signature using HeartbeatAddress's partkey, thereby showing it is online. - HbProof crypto.OneTimeSignature `codec:"hbprf"` + HbProof crypto.HeartbeatProof `codec:"hbprf"` // HbSeed must be the block seed for the block before this transaction's // firstValid. It is supplied in the transaction so that Proof can be diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index f7ea0e8fbb..edc229bffe 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -3080,7 +3080,7 @@ func (z *HeartbeatTxnFields) MsgIsZero() bool { // MaxSize returns a maximum valid message size for this message type func HeartbeatTxnFieldsMaxSize() (s int) { - s = 1 + 5 + basics.AddressMaxSize() + 6 + crypto.OneTimeSignatureMaxSize() + 5 + committee.SeedMaxSize() + s = 1 + 5 + basics.AddressMaxSize() + 6 + crypto.HeartbeatProofMaxSize() + 5 + committee.SeedMaxSize() return } @@ -6935,7 +6935,7 @@ func TransactionMaxSize() (s int) { s += 5 // Calculating size of slice: z.ApplicationCallTxnFields.ForeignAssets s += msgp.ArrayHeaderSize + ((encodedMaxForeignAssets) * (basics.AssetIndexMaxSize())) - s += 5 + basics.StateSchemaMaxSize() + 5 + basics.StateSchemaMaxSize() + 5 + msgp.BytesPrefixSize + config.MaxAvailableAppProgramLen + 5 + msgp.BytesPrefixSize + config.MaxAvailableAppProgramLen + 5 + msgp.Uint32Size + 7 + protocol.StateProofTypeMaxSize() + 3 + stateproof.StateProofMaxSize() + 6 + stateproofmsg.MessageMaxSize() + 5 + basics.AddressMaxSize() + 6 + crypto.OneTimeSignatureMaxSize() + 5 + committee.SeedMaxSize() + s += 5 + basics.StateSchemaMaxSize() + 5 + basics.StateSchemaMaxSize() + 5 + msgp.BytesPrefixSize + config.MaxAvailableAppProgramLen + 5 + msgp.BytesPrefixSize + config.MaxAvailableAppProgramLen + 5 + msgp.Uint32Size + 7 + protocol.StateProofTypeMaxSize() + 3 + stateproof.StateProofMaxSize() + 6 + stateproofmsg.MessageMaxSize() + 5 + basics.AddressMaxSize() + 6 + crypto.HeartbeatProofMaxSize() + 5 + committee.SeedMaxSize() return } diff --git a/data/txntest/txn.go b/data/txntest/txn.go index 5b07ad03d5..d734f47576 100644 --- a/data/txntest/txn.go +++ b/data/txntest/txn.go @@ -94,7 +94,7 @@ type Txn struct { StateProofMsg stateproofmsg.Message HbAddress basics.Address - HbProof crypto.OneTimeSignature + HbProof crypto.HeartbeatProof HbSeed committee.Seed } diff --git a/ledger/apply/heartbeat.go b/ledger/apply/heartbeat.go index 3ef01d408f..a0e57849bf 100644 --- a/ledger/apply/heartbeat.go +++ b/ledger/apply/heartbeat.go @@ -108,7 +108,7 @@ func Heartbeat(hb transactions.HeartbeatTxnFields, header transactions.Header, b return fmt.Errorf("provided seed %v does not match round %d's seed %v", hb.HbSeed, header.FirstValid-1, hdr.Seed) } - if !sv.Verify(id, hdr.Seed, hb.HbProof) { + if !sv.Verify(id, hdr.Seed, hb.HbProof.ToOneTimeSignature()) { return fmt.Errorf("heartbeat failed verification with VoteID %v", sv) } diff --git a/ledger/apply/heartbeat_test.go b/ledger/apply/heartbeat_test.go index 3f43025a7c..f4468ab6e7 100644 --- a/ledger/apply/heartbeat_test.go +++ b/ledger/apply/heartbeat_test.go @@ -73,7 +73,7 @@ func TestHeartbeat(t *testing.T) { FirstValid: fv, LastValid: lv, HbAddress: voter, - HbProof: otss.Sign(id, seed), + HbProof: otss.Sign(id, seed).ToHeartbeatProof(), HbSeed: seed, } @@ -174,7 +174,7 @@ func TestCheapRules(t *testing.T) { Note: tc.note, RekeyTo: tc.rekey, HbAddress: voter, - HbProof: otss.Sign(id, seed), + HbProof: otss.Sign(id, seed).ToHeartbeatProof(), HbSeed: seed, }