diff --git a/data/transactions/heartbeat.go b/data/transactions/heartbeat.go index 5a36343339..33b1e19ac2 100644 --- a/data/transactions/heartbeat.go +++ b/data/transactions/heartbeat.go @@ -28,15 +28,22 @@ import ( type HeartbeatTxnFields struct { _struct struct{} `codec:",omitempty,omitemptyarray"` - // HeartbeatAddress is the account this txn is proving onlineness for. + // HbAddress is the account this txn is proving onlineness for. HbAddress basics.Address `codec:"hbad"` // HbProof is a signature using HeartbeatAddress's partkey, thereby showing it is online. HbProof crypto.HeartbeatProof `codec:"hbprf"` + // The final three fields are included to allow early, concurrent check of + // the HbProof. + // HbSeed must be the block seed for the this transaction's firstValid - // block. It is supplied in the transaction so that Proof can be checked at - // submit time without a ledger lookup, and must be checked at evaluation - // time for equality with the actual blockseed. + // block. It is the message that must be signed with HbAddress's part key. HbSeed committee.Seed `codec:"hbsd"` + + // HbVoteID must match the HbAddress account's current VoteID. + HbVoteID crypto.OneTimeSignatureVerifier `codec:"hbvid"` + + // HbKeyDilution must match HbAddress account's current KeyDilution. + HbKeyDilution uint64 `codec:"hbkd"` } diff --git a/data/transactions/msgp_gen.go b/data/transactions/msgp_gen.go index 0b644da818..9e901342c8 100644 --- a/data/transactions/msgp_gen.go +++ b/data/transactions/msgp_gen.go @@ -2922,20 +2922,28 @@ func HeaderMaxSize() (s int) { func (z *HeartbeatTxnFields) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0001Len := uint32(3) - var zb0001Mask uint8 /* 4 bits */ + zb0001Len := uint32(5) + var zb0001Mask uint8 /* 6 bits */ if (*z).HbAddress.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x2 } - if (*z).HbProof.MsgIsZero() { + if (*z).HbKeyDilution == 0 { zb0001Len-- zb0001Mask |= 0x4 } - if (*z).HbSeed.MsgIsZero() { + if (*z).HbProof.MsgIsZero() { zb0001Len-- zb0001Mask |= 0x8 } + if (*z).HbSeed.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x10 + } + if (*z).HbVoteID.MsgIsZero() { + zb0001Len-- + zb0001Mask |= 0x20 + } // variable map header, size zb0001Len o = append(o, 0x80|uint8(zb0001Len)) if zb0001Len != 0 { @@ -2945,15 +2953,25 @@ func (z *HeartbeatTxnFields) MarshalMsg(b []byte) (o []byte) { o = (*z).HbAddress.MarshalMsg(o) } if (zb0001Mask & 0x4) == 0 { // if not empty + // string "hbkd" + o = append(o, 0xa4, 0x68, 0x62, 0x6b, 0x64) + o = msgp.AppendUint64(o, (*z).HbKeyDilution) + } + if (zb0001Mask & 0x8) == 0 { // if not empty // string "hbprf" o = append(o, 0xa5, 0x68, 0x62, 0x70, 0x72, 0x66) o = (*z).HbProof.MarshalMsg(o) } - if (zb0001Mask & 0x8) == 0 { // if not empty + if (zb0001Mask & 0x10) == 0 { // if not empty // string "hbsd" o = append(o, 0xa4, 0x68, 0x62, 0x73, 0x64) o = (*z).HbSeed.MarshalMsg(o) } + if (zb0001Mask & 0x20) == 0 { // if not empty + // string "hbvid" + o = append(o, 0xa5, 0x68, 0x62, 0x76, 0x69, 0x64) + o = (*z).HbVoteID.MarshalMsg(o) + } } return } @@ -3005,6 +3023,22 @@ func (z *HeartbeatTxnFields) UnmarshalMsgWithState(bts []byte, st msgp.Unmarshal return } } + if zb0001 > 0 { + zb0001-- + bts, err = (*z).HbVoteID.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "HbVoteID") + return + } + } + if zb0001 > 0 { + zb0001-- + (*z).HbKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "HbKeyDilution") + return + } + } if zb0001 > 0 { err = msgp.ErrTooManyArrayFields(zb0001) if err != nil { @@ -3046,6 +3080,18 @@ func (z *HeartbeatTxnFields) UnmarshalMsgWithState(bts []byte, st msgp.Unmarshal err = msgp.WrapError(err, "HbSeed") return } + case "hbvid": + bts, err = (*z).HbVoteID.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "HbVoteID") + return + } + case "hbkd": + (*z).HbKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "HbKeyDilution") + return + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -3069,18 +3115,18 @@ func (_ *HeartbeatTxnFields) CanUnmarshalMsg(z interface{}) bool { // Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message func (z *HeartbeatTxnFields) Msgsize() (s int) { - s = 1 + 5 + (*z).HbAddress.Msgsize() + 6 + (*z).HbProof.Msgsize() + 5 + (*z).HbSeed.Msgsize() + s = 1 + 5 + (*z).HbAddress.Msgsize() + 6 + (*z).HbProof.Msgsize() + 5 + (*z).HbSeed.Msgsize() + 6 + (*z).HbVoteID.Msgsize() + 5 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value func (z *HeartbeatTxnFields) MsgIsZero() bool { - return ((*z).HbAddress.MsgIsZero()) && ((*z).HbProof.MsgIsZero()) && ((*z).HbSeed.MsgIsZero()) + return ((*z).HbAddress.MsgIsZero()) && ((*z).HbProof.MsgIsZero()) && ((*z).HbSeed.MsgIsZero()) && ((*z).HbVoteID.MsgIsZero()) && ((*z).HbKeyDilution == 0) } // MaxSize returns a maximum valid message size for this message type func HeartbeatTxnFieldsMaxSize() (s int) { - s = 1 + 5 + basics.AddressMaxSize() + 6 + crypto.HeartbeatProofMaxSize() + 5 + committee.SeedMaxSize() + s = 1 + 5 + basics.AddressMaxSize() + 6 + crypto.HeartbeatProofMaxSize() + 5 + committee.SeedMaxSize() + 6 + crypto.OneTimeSignatureVerifierMaxSize() + 5 + msgp.Uint64Size return } @@ -5159,8 +5205,8 @@ func StateProofTxnFieldsMaxSize() (s int) { func (z *Transaction) MarshalMsg(b []byte) (o []byte) { o = msgp.Require(b, z.Msgsize()) // omitempty: check for empty values - zb0007Len := uint32(49) - var zb0007Mask uint64 /* 59 bits */ + zb0007Len := uint32(51) + var zb0007Mask uint64 /* 61 bits */ if (*z).AssetTransferTxnFields.AssetAmount == 0 { zb0007Len-- zb0007Mask |= 0x400 @@ -5277,86 +5323,94 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte) { zb0007Len-- zb0007Mask |= 0x4000000000 } - if (*z).HeartbeatTxnFields.HbProof.MsgIsZero() { + if (*z).HeartbeatTxnFields.HbKeyDilution == 0 { zb0007Len-- zb0007Mask |= 0x8000000000 } - if (*z).HeartbeatTxnFields.HbSeed.MsgIsZero() { + if (*z).HeartbeatTxnFields.HbProof.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x10000000000 } - if (*z).Header.LastValid.MsgIsZero() { + if (*z).HeartbeatTxnFields.HbSeed.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x20000000000 } - if (*z).Header.Lease == ([32]byte{}) { + if (*z).HeartbeatTxnFields.HbVoteID.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x40000000000 } - if (*z).KeyregTxnFields.Nonparticipation == false { + if (*z).Header.LastValid.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x80000000000 } - if len((*z).Header.Note) == 0 { + if (*z).Header.Lease == ([32]byte{}) { zb0007Len-- zb0007Mask |= 0x100000000000 } - if (*z).PaymentTxnFields.Receiver.MsgIsZero() { + if (*z).KeyregTxnFields.Nonparticipation == false { zb0007Len-- zb0007Mask |= 0x200000000000 } - if (*z).Header.RekeyTo.MsgIsZero() { + if len((*z).Header.Note) == 0 { zb0007Len-- zb0007Mask |= 0x400000000000 } - if (*z).KeyregTxnFields.SelectionPK.MsgIsZero() { + if (*z).PaymentTxnFields.Receiver.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x800000000000 } - if (*z).Header.Sender.MsgIsZero() { + if (*z).Header.RekeyTo.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x1000000000000 } - if (*z).StateProofTxnFields.StateProof.MsgIsZero() { + if (*z).KeyregTxnFields.SelectionPK.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x2000000000000 } - if (*z).StateProofTxnFields.Message.MsgIsZero() { + if (*z).Header.Sender.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x4000000000000 } - if (*z).KeyregTxnFields.StateProofPK.MsgIsZero() { + if (*z).StateProofTxnFields.StateProof.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x8000000000000 } - if (*z).StateProofTxnFields.StateProofType.MsgIsZero() { + if (*z).StateProofTxnFields.Message.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x10000000000000 } - if (*z).Type.MsgIsZero() { + if (*z).KeyregTxnFields.StateProofPK.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x20000000000000 } - if (*z).KeyregTxnFields.VoteFirst.MsgIsZero() { + if (*z).StateProofTxnFields.StateProofType.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x40000000000000 } - if (*z).KeyregTxnFields.VoteKeyDilution == 0 { + if (*z).Type.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x80000000000000 } - if (*z).KeyregTxnFields.VotePK.MsgIsZero() { + if (*z).KeyregTxnFields.VoteFirst.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x100000000000000 } - if (*z).KeyregTxnFields.VoteLast.MsgIsZero() { + if (*z).KeyregTxnFields.VoteKeyDilution == 0 { zb0007Len-- zb0007Mask |= 0x200000000000000 } - if (*z).AssetTransferTxnFields.XferAsset.MsgIsZero() { + if (*z).KeyregTxnFields.VotePK.MsgIsZero() { zb0007Len-- zb0007Mask |= 0x400000000000000 } + if (*z).KeyregTxnFields.VoteLast.MsgIsZero() { + zb0007Len-- + zb0007Mask |= 0x800000000000000 + } + if (*z).AssetTransferTxnFields.XferAsset.MsgIsZero() { + zb0007Len-- + zb0007Mask |= 0x1000000000000000 + } // variable map header, size zb0007Len o = msgp.AppendMapHeader(o, zb0007Len) if zb0007Len != 0 { @@ -5563,101 +5617,111 @@ func (z *Transaction) MarshalMsg(b []byte) (o []byte) { o = (*z).HeartbeatTxnFields.HbAddress.MarshalMsg(o) } if (zb0007Mask & 0x8000000000) == 0 { // if not empty + // string "hbkd" + o = append(o, 0xa4, 0x68, 0x62, 0x6b, 0x64) + o = msgp.AppendUint64(o, (*z).HeartbeatTxnFields.HbKeyDilution) + } + if (zb0007Mask & 0x10000000000) == 0 { // if not empty // string "hbprf" o = append(o, 0xa5, 0x68, 0x62, 0x70, 0x72, 0x66) o = (*z).HeartbeatTxnFields.HbProof.MarshalMsg(o) } - if (zb0007Mask & 0x10000000000) == 0 { // if not empty + if (zb0007Mask & 0x20000000000) == 0 { // if not empty // string "hbsd" o = append(o, 0xa4, 0x68, 0x62, 0x73, 0x64) o = (*z).HeartbeatTxnFields.HbSeed.MarshalMsg(o) } - if (zb0007Mask & 0x20000000000) == 0 { // if not empty + if (zb0007Mask & 0x40000000000) == 0 { // if not empty + // string "hbvid" + o = append(o, 0xa5, 0x68, 0x62, 0x76, 0x69, 0x64) + o = (*z).HeartbeatTxnFields.HbVoteID.MarshalMsg(o) + } + if (zb0007Mask & 0x80000000000) == 0 { // if not empty // string "lv" o = append(o, 0xa2, 0x6c, 0x76) o = (*z).Header.LastValid.MarshalMsg(o) } - if (zb0007Mask & 0x40000000000) == 0 { // if not empty + if (zb0007Mask & 0x100000000000) == 0 { // if not empty // string "lx" o = append(o, 0xa2, 0x6c, 0x78) o = msgp.AppendBytes(o, ((*z).Header.Lease)[:]) } - if (zb0007Mask & 0x80000000000) == 0 { // if not empty + if (zb0007Mask & 0x200000000000) == 0 { // if not empty // string "nonpart" o = append(o, 0xa7, 0x6e, 0x6f, 0x6e, 0x70, 0x61, 0x72, 0x74) o = msgp.AppendBool(o, (*z).KeyregTxnFields.Nonparticipation) } - if (zb0007Mask & 0x100000000000) == 0 { // if not empty + if (zb0007Mask & 0x400000000000) == 0 { // if not empty // string "note" o = append(o, 0xa4, 0x6e, 0x6f, 0x74, 0x65) o = msgp.AppendBytes(o, (*z).Header.Note) } - if (zb0007Mask & 0x200000000000) == 0 { // if not empty + if (zb0007Mask & 0x800000000000) == 0 { // if not empty // string "rcv" o = append(o, 0xa3, 0x72, 0x63, 0x76) o = (*z).PaymentTxnFields.Receiver.MarshalMsg(o) } - if (zb0007Mask & 0x400000000000) == 0 { // if not empty + if (zb0007Mask & 0x1000000000000) == 0 { // if not empty // string "rekey" o = append(o, 0xa5, 0x72, 0x65, 0x6b, 0x65, 0x79) o = (*z).Header.RekeyTo.MarshalMsg(o) } - if (zb0007Mask & 0x800000000000) == 0 { // if not empty + if (zb0007Mask & 0x2000000000000) == 0 { // if not empty // string "selkey" o = append(o, 0xa6, 0x73, 0x65, 0x6c, 0x6b, 0x65, 0x79) o = (*z).KeyregTxnFields.SelectionPK.MarshalMsg(o) } - if (zb0007Mask & 0x1000000000000) == 0 { // if not empty + if (zb0007Mask & 0x4000000000000) == 0 { // if not empty // string "snd" o = append(o, 0xa3, 0x73, 0x6e, 0x64) o = (*z).Header.Sender.MarshalMsg(o) } - if (zb0007Mask & 0x2000000000000) == 0 { // if not empty + if (zb0007Mask & 0x8000000000000) == 0 { // if not empty // string "sp" o = append(o, 0xa2, 0x73, 0x70) o = (*z).StateProofTxnFields.StateProof.MarshalMsg(o) } - if (zb0007Mask & 0x4000000000000) == 0 { // if not empty + if (zb0007Mask & 0x10000000000000) == 0 { // if not empty // string "spmsg" o = append(o, 0xa5, 0x73, 0x70, 0x6d, 0x73, 0x67) o = (*z).StateProofTxnFields.Message.MarshalMsg(o) } - if (zb0007Mask & 0x8000000000000) == 0 { // if not empty + if (zb0007Mask & 0x20000000000000) == 0 { // if not empty // string "sprfkey" o = append(o, 0xa7, 0x73, 0x70, 0x72, 0x66, 0x6b, 0x65, 0x79) o = (*z).KeyregTxnFields.StateProofPK.MarshalMsg(o) } - if (zb0007Mask & 0x10000000000000) == 0 { // if not empty + if (zb0007Mask & 0x40000000000000) == 0 { // if not empty // string "sptype" o = append(o, 0xa6, 0x73, 0x70, 0x74, 0x79, 0x70, 0x65) o = (*z).StateProofTxnFields.StateProofType.MarshalMsg(o) } - if (zb0007Mask & 0x20000000000000) == 0 { // if not empty + if (zb0007Mask & 0x80000000000000) == 0 { // if not empty // string "type" o = append(o, 0xa4, 0x74, 0x79, 0x70, 0x65) o = (*z).Type.MarshalMsg(o) } - if (zb0007Mask & 0x40000000000000) == 0 { // if not empty + if (zb0007Mask & 0x100000000000000) == 0 { // if not empty // string "votefst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x66, 0x73, 0x74) o = (*z).KeyregTxnFields.VoteFirst.MarshalMsg(o) } - if (zb0007Mask & 0x80000000000000) == 0 { // if not empty + if (zb0007Mask & 0x200000000000000) == 0 { // if not empty // string "votekd" o = append(o, 0xa6, 0x76, 0x6f, 0x74, 0x65, 0x6b, 0x64) o = msgp.AppendUint64(o, (*z).KeyregTxnFields.VoteKeyDilution) } - if (zb0007Mask & 0x100000000000000) == 0 { // if not empty + if (zb0007Mask & 0x400000000000000) == 0 { // if not empty // string "votekey" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x6b, 0x65, 0x79) o = (*z).KeyregTxnFields.VotePK.MarshalMsg(o) } - if (zb0007Mask & 0x200000000000000) == 0 { // if not empty + if (zb0007Mask & 0x800000000000000) == 0 { // if not empty // string "votelst" o = append(o, 0xa7, 0x76, 0x6f, 0x74, 0x65, 0x6c, 0x73, 0x74) o = (*z).KeyregTxnFields.VoteLast.MarshalMsg(o) } - if (zb0007Mask & 0x400000000000000) == 0 { // if not empty + if (zb0007Mask & 0x1000000000000000) == 0 { // if not empty // string "xaid" o = append(o, 0xa4, 0x78, 0x61, 0x69, 0x64) o = (*z).AssetTransferTxnFields.XferAsset.MarshalMsg(o) @@ -6314,6 +6378,22 @@ func (z *Transaction) UnmarshalMsgWithState(bts []byte, st msgp.UnmarshalState) return } } + if zb0007 > 0 { + zb0007-- + bts, err = (*z).HeartbeatTxnFields.HbVoteID.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "HbVoteID") + return + } + } + if zb0007 > 0 { + zb0007-- + (*z).HeartbeatTxnFields.HbKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "struct-from-array", "HbKeyDilution") + return + } + } if zb0007 > 0 { err = msgp.ErrTooManyArrayFields(zb0007) if err != nil { @@ -6864,6 +6944,18 @@ func (z *Transaction) UnmarshalMsgWithState(bts []byte, st msgp.UnmarshalState) err = msgp.WrapError(err, "HbSeed") return } + case "hbvid": + bts, err = (*z).HeartbeatTxnFields.HbVoteID.UnmarshalMsgWithState(bts, st) + if err != nil { + err = msgp.WrapError(err, "HbVoteID") + return + } + case "hbkd": + (*z).HeartbeatTxnFields.HbKeyDilution, bts, err = msgp.ReadUint64Bytes(bts) + if err != nil { + err = msgp.WrapError(err, "HbKeyDilution") + return + } default: err = msgp.ErrNoField(string(field)) if err != nil { @@ -6907,13 +6999,13 @@ func (z *Transaction) Msgsize() (s int) { for zb0006 := range (*z).ApplicationCallTxnFields.ForeignAssets { s += (*z).ApplicationCallTxnFields.ForeignAssets[zb0006].Msgsize() } - s += 5 + (*z).ApplicationCallTxnFields.LocalStateSchema.Msgsize() + 5 + (*z).ApplicationCallTxnFields.GlobalStateSchema.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ApprovalProgram) + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ClearStateProgram) + 5 + msgp.Uint32Size + 7 + (*z).StateProofTxnFields.StateProofType.Msgsize() + 3 + (*z).StateProofTxnFields.StateProof.Msgsize() + 6 + (*z).StateProofTxnFields.Message.Msgsize() + 5 + (*z).HeartbeatTxnFields.HbAddress.Msgsize() + 6 + (*z).HeartbeatTxnFields.HbProof.Msgsize() + 5 + (*z).HeartbeatTxnFields.HbSeed.Msgsize() + s += 5 + (*z).ApplicationCallTxnFields.LocalStateSchema.Msgsize() + 5 + (*z).ApplicationCallTxnFields.GlobalStateSchema.Msgsize() + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ApprovalProgram) + 5 + msgp.BytesPrefixSize + len((*z).ApplicationCallTxnFields.ClearStateProgram) + 5 + msgp.Uint32Size + 7 + (*z).StateProofTxnFields.StateProofType.Msgsize() + 3 + (*z).StateProofTxnFields.StateProof.Msgsize() + 6 + (*z).StateProofTxnFields.Message.Msgsize() + 5 + (*z).HeartbeatTxnFields.HbAddress.Msgsize() + 6 + (*z).HeartbeatTxnFields.HbProof.Msgsize() + 5 + (*z).HeartbeatTxnFields.HbSeed.Msgsize() + 6 + (*z).HeartbeatTxnFields.HbVoteID.Msgsize() + 5 + msgp.Uint64Size return } // MsgIsZero returns whether this is a zero value func (z *Transaction) MsgIsZero() bool { - return ((*z).Type.MsgIsZero()) && ((*z).Header.Sender.MsgIsZero()) && ((*z).Header.Fee.MsgIsZero()) && ((*z).Header.FirstValid.MsgIsZero()) && ((*z).Header.LastValid.MsgIsZero()) && (len((*z).Header.Note) == 0) && ((*z).Header.GenesisID == "") && ((*z).Header.GenesisHash.MsgIsZero()) && ((*z).Header.Group.MsgIsZero()) && ((*z).Header.Lease == ([32]byte{})) && ((*z).Header.RekeyTo.MsgIsZero()) && ((*z).KeyregTxnFields.VotePK.MsgIsZero()) && ((*z).KeyregTxnFields.SelectionPK.MsgIsZero()) && ((*z).KeyregTxnFields.StateProofPK.MsgIsZero()) && ((*z).KeyregTxnFields.VoteFirst.MsgIsZero()) && ((*z).KeyregTxnFields.VoteLast.MsgIsZero()) && ((*z).KeyregTxnFields.VoteKeyDilution == 0) && ((*z).KeyregTxnFields.Nonparticipation == false) && ((*z).PaymentTxnFields.Receiver.MsgIsZero()) && ((*z).PaymentTxnFields.Amount.MsgIsZero()) && ((*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero()) && ((*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero()) && ((*z).AssetConfigTxnFields.AssetParams.MsgIsZero()) && ((*z).AssetTransferTxnFields.XferAsset.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetAmount == 0) && ((*z).AssetTransferTxnFields.AssetSender.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero()) && ((*z).AssetFreezeTxnFields.AssetFrozen == false) && ((*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero()) && ((*z).ApplicationCallTxnFields.OnCompletion == 0) && (len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0) && (len((*z).ApplicationCallTxnFields.Accounts) == 0) && (len((*z).ApplicationCallTxnFields.ForeignApps) == 0) && (len((*z).ApplicationCallTxnFields.Boxes) == 0) && (len((*z).ApplicationCallTxnFields.ForeignAssets) == 0) && ((*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero()) && ((*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero()) && (len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0) && (len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0) && ((*z).ApplicationCallTxnFields.ExtraProgramPages == 0) && ((*z).StateProofTxnFields.StateProofType.MsgIsZero()) && ((*z).StateProofTxnFields.StateProof.MsgIsZero()) && ((*z).StateProofTxnFields.Message.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbAddress.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbProof.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbSeed.MsgIsZero()) + return ((*z).Type.MsgIsZero()) && ((*z).Header.Sender.MsgIsZero()) && ((*z).Header.Fee.MsgIsZero()) && ((*z).Header.FirstValid.MsgIsZero()) && ((*z).Header.LastValid.MsgIsZero()) && (len((*z).Header.Note) == 0) && ((*z).Header.GenesisID == "") && ((*z).Header.GenesisHash.MsgIsZero()) && ((*z).Header.Group.MsgIsZero()) && ((*z).Header.Lease == ([32]byte{})) && ((*z).Header.RekeyTo.MsgIsZero()) && ((*z).KeyregTxnFields.VotePK.MsgIsZero()) && ((*z).KeyregTxnFields.SelectionPK.MsgIsZero()) && ((*z).KeyregTxnFields.StateProofPK.MsgIsZero()) && ((*z).KeyregTxnFields.VoteFirst.MsgIsZero()) && ((*z).KeyregTxnFields.VoteLast.MsgIsZero()) && ((*z).KeyregTxnFields.VoteKeyDilution == 0) && ((*z).KeyregTxnFields.Nonparticipation == false) && ((*z).PaymentTxnFields.Receiver.MsgIsZero()) && ((*z).PaymentTxnFields.Amount.MsgIsZero()) && ((*z).PaymentTxnFields.CloseRemainderTo.MsgIsZero()) && ((*z).AssetConfigTxnFields.ConfigAsset.MsgIsZero()) && ((*z).AssetConfigTxnFields.AssetParams.MsgIsZero()) && ((*z).AssetTransferTxnFields.XferAsset.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetAmount == 0) && ((*z).AssetTransferTxnFields.AssetSender.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetReceiver.MsgIsZero()) && ((*z).AssetTransferTxnFields.AssetCloseTo.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAccount.MsgIsZero()) && ((*z).AssetFreezeTxnFields.FreezeAsset.MsgIsZero()) && ((*z).AssetFreezeTxnFields.AssetFrozen == false) && ((*z).ApplicationCallTxnFields.ApplicationID.MsgIsZero()) && ((*z).ApplicationCallTxnFields.OnCompletion == 0) && (len((*z).ApplicationCallTxnFields.ApplicationArgs) == 0) && (len((*z).ApplicationCallTxnFields.Accounts) == 0) && (len((*z).ApplicationCallTxnFields.ForeignApps) == 0) && (len((*z).ApplicationCallTxnFields.Boxes) == 0) && (len((*z).ApplicationCallTxnFields.ForeignAssets) == 0) && ((*z).ApplicationCallTxnFields.LocalStateSchema.MsgIsZero()) && ((*z).ApplicationCallTxnFields.GlobalStateSchema.MsgIsZero()) && (len((*z).ApplicationCallTxnFields.ApprovalProgram) == 0) && (len((*z).ApplicationCallTxnFields.ClearStateProgram) == 0) && ((*z).ApplicationCallTxnFields.ExtraProgramPages == 0) && ((*z).StateProofTxnFields.StateProofType.MsgIsZero()) && ((*z).StateProofTxnFields.StateProof.MsgIsZero()) && ((*z).StateProofTxnFields.Message.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbAddress.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbProof.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbSeed.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbVoteID.MsgIsZero()) && ((*z).HeartbeatTxnFields.HbKeyDilution == 0) } // MaxSize returns a maximum valid message size for this message type @@ -6935,7 +7027,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.HeartbeatProofMaxSize() + 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() + 6 + crypto.OneTimeSignatureVerifierMaxSize() + 5 + msgp.Uint64Size return } diff --git a/data/transactions/transaction.go b/data/transactions/transaction.go index 83f9bc6f62..cd7aab7373 100644 --- a/data/transactions/transaction.go +++ b/data/transactions/transaction.go @@ -27,6 +27,7 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/protocol" ) @@ -571,6 +572,37 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa return fmt.Errorf("heartbeat transaction not supported") } + // If this is a free/cheap heartbeat, it must be very simple. + if tx.Fee.Raw < proto.MinTxnFee && tx.Group.IsZero() { + kind := "free" + if tx.Fee.Raw > 0 { + kind = "cheap" + } + + if len(tx.Note) > 0 { + return fmt.Errorf("tx.Note is set in %s heartbeat", kind) + } + if tx.Lease != [32]byte{} { + return fmt.Errorf("tx.Lease is set in %s heartbeat", kind) + } + if !tx.RekeyTo.IsZero() { + return fmt.Errorf("tx.RekeyTo is set in %s heartbeat", kind) + } + } + + if (tx.HbProof == crypto.HeartbeatProof{}) { + return errors.New("tx.HbProof is empty") + } + if (tx.HbSeed == committee.Seed{}) { + return errors.New("tx.HbSeed is empty") + } + if tx.HbVoteID.IsEmpty() { + return errors.New("tx.HbVoteID is empty") + } + if tx.HbKeyDilution == 0 { + return errors.New("tx.HbKeyDilution is zero") + } + default: return fmt.Errorf("unknown tx type %v", tx.Type) } diff --git a/data/transactions/transaction_test.go b/data/transactions/transaction_test.go index 1dbb2e316a..6218c9820e 100644 --- a/data/transactions/transaction_test.go +++ b/data/transactions/transaction_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/algorand/go-algorand/config" @@ -29,6 +30,7 @@ import ( "github.com/algorand/go-algorand/crypto/merklesignature" "github.com/algorand/go-algorand/crypto/stateproof" "github.com/algorand/go-algorand/data/basics" + "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/data/stateproofmsg" "github.com/algorand/go-algorand/protocol" "github.com/algorand/go-algorand/test/partitiontest" @@ -603,13 +605,144 @@ func TestWellFormedErrors(t *testing.T) { tx: Transaction{ Type: protocol.HeartbeatTx, Header: okHeader, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbSeed: committee.Seed{0x02}, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + HbKeyDilution: 10, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.HbProof is empty"), + }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: okHeader, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + HbKeyDilution: 10, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.HbSeed is empty"), + }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: okHeader, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbSeed: committee.Seed{0x02}, + HbKeyDilution: 10, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.HbVoteID is empty"), + }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: okHeader, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbSeed: committee.Seed{0x02}, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.HbKeyDilution is zero"), + }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: okHeader, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbSeed: committee.Seed{0x02}, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + HbKeyDilution: 10, + }, }, proto: futureProto, }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: Header{ + Sender: addr1, + Fee: basics.MicroAlgos{Raw: 100}, + LastValid: 105, + FirstValid: 100, + Note: []byte{0x01}, + }, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbSeed: committee.Seed{0x02}, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + HbKeyDilution: 10, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.Note is set in cheap heartbeat"), + }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: Header{ + Sender: addr1, + Fee: basics.MicroAlgos{Raw: 100}, + LastValid: 105, + FirstValid: 100, + Lease: [32]byte{0x01}, + }, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbSeed: committee.Seed{0x02}, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + HbKeyDilution: 10, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.Lease is set in cheap heartbeat"), + }, + { + tx: Transaction{ + Type: protocol.HeartbeatTx, + Header: Header{ + Sender: addr1, + LastValid: 105, + FirstValid: 100, + RekeyTo: [32]byte{0x01}, + }, + HeartbeatTxnFields: HeartbeatTxnFields{ + HbProof: crypto.HeartbeatProof{ + Sig: [64]byte{0x01}, + }, + HbSeed: committee.Seed{0x02}, + HbVoteID: crypto.OneTimeSignatureVerifier{0x03}, + HbKeyDilution: 10, + }, + }, + proto: futureProto, + expectedError: fmt.Errorf("tx.RekeyTo is set in free heartbeat"), + }, } for _, usecase := range usecases { err := usecase.tx.WellFormed(SpecialAddresses{}, usecase.proto) - require.Equal(t, usecase.expectedError, err) + assert.Equal(t, usecase.expectedError, err) } } diff --git a/data/transactions/verify/txn.go b/data/transactions/verify/txn.go index 7862d71b89..d5a9f68a97 100644 --- a/data/transactions/verify/txn.go +++ b/data/transactions/verify/txn.go @@ -310,6 +310,15 @@ func stxnCoreChecks(gi int, groupCtx *GroupContext, batchVerifier crypto.BatchVe return err } + if s.Txn.Type == protocol.HeartbeatTx { + id := basics.OneTimeIDForRound(s.Txn.LastValid, s.Txn.HbKeyDilution) + offsetID := crypto.OneTimeSignatureSubkeyOffsetID{SubKeyPK: s.Txn.HbProof.PK, Batch: id.Batch, Offset: id.Offset} + batchID := crypto.OneTimeSignatureSubkeyBatchID{SubKeyPK: s.Txn.HbProof.PK2, Batch: id.Batch} + batchVerifier.EnqueueSignature(crypto.PublicKey(s.Txn.HbVoteID), batchID, crypto.Signature(s.Txn.HbProof.PK2Sig)) + batchVerifier.EnqueueSignature(crypto.PublicKey(batchID.SubKeyPK), offsetID, crypto.Signature(s.Txn.HbProof.PK1Sig)) + batchVerifier.EnqueueSignature(crypto.PublicKey(offsetID.SubKeyPK), s.Txn.HbSeed, crypto.Signature(s.Txn.HbProof.Sig)) + } + switch sigType { case regularSig: batchVerifier.EnqueueSignature(crypto.SignatureVerifier(s.Authorizer()), s.Txn, s.Sig) diff --git a/data/transactions/verify/txn_test.go b/data/transactions/verify/txn_test.go index 7320151565..96e58118c3 100644 --- a/data/transactions/verify/txn_test.go +++ b/data/transactions/verify/txn_test.go @@ -30,6 +30,7 @@ import ( "github.com/algorand/go-algorand/crypto" "github.com/algorand/go-algorand/data/basics" "github.com/algorand/go-algorand/data/bookkeeping" + "github.com/algorand/go-algorand/data/committee" "github.com/algorand/go-algorand/data/transactions" "github.com/algorand/go-algorand/data/transactions/logic" "github.com/algorand/go-algorand/data/transactions/logic/mocktracer" @@ -94,6 +95,38 @@ func keypair() *crypto.SignatureSecrets { return s } +func createHeartbeatTxn(fv basics.Round, t *testing.T) transactions.SignedTxn { + secrets, addrs, _ := generateAccounts(1) + + kd := uint64(111) + lv := fv + 15 + id := basics.OneTimeIDForRound(lv, kd) + + seed := committee.Seed{0x33} + otss := crypto.GenerateOneTimeSignatureSecrets(0, kd) + + txn := transactions.Transaction{ + Type: "hb", + Header: transactions.Header{ + Sender: addrs[0], + FirstValid: fv, + LastValid: lv, + }, + HeartbeatTxnFields: transactions.HeartbeatTxnFields{ + HbProof: otss.Sign(id, seed).ToHeartbeatProof(), + HbSeed: seed, + HbVoteID: otss.OneTimeSignatureVerifier, + HbKeyDilution: kd, + }, + } + + hb := transactions.SignedTxn{ + Sig: secrets[0].Sign(txn), + Txn: txn, + } + return hb +} + func generateMultiSigTxn(numTxs, numAccs int, blockRound basics.Round, t *testing.T) ([]transactions.Transaction, []transactions.SignedTxn, []*crypto.SignatureSecrets, []basics.Address) { secrets, addresses, pks, multiAddress := generateMultiSigAccounts(t, numAccs) @@ -864,6 +897,38 @@ func TestTxnGroupCacheUpdateMultiSig(t *testing.T) { verifyGroup(t, txnGroups, &blkHdr, breakSignatureFunc, restoreSignatureFunc, crypto.ErrBatchHasFailedSigs.Error()) } +// TestTxnHeartbeat makes sure that a heartbeat transaction is valid (and added +// to the cache) only if the normal outer signature is valid AND the inner +// HbProof is valid. +func TestTxnHeartbeat(t *testing.T) { + partitiontest.PartitionTest(t) + + blkHdr := createDummyBlockHeader(protocol.ConsensusFuture) + + txnGroups := make([][]transactions.SignedTxn, 2) // verifyGroup requires at least 2 + for i := 0; i < len(txnGroups); i++ { + txnGroups[i] = make([]transactions.SignedTxn, 1) + txnGroups[i][0] = createHeartbeatTxn(blkHdr.Round-1, t) + } + breakSignatureFunc := func(txn *transactions.SignedTxn) { + txn.Sig[0]++ + } + restoreSignatureFunc := func(txn *transactions.SignedTxn) { + txn.Sig[0]-- + } + // This shows the outer signature must be correct + verifyGroup(t, txnGroups, &blkHdr, breakSignatureFunc, restoreSignatureFunc, crypto.ErrBatchHasFailedSigs.Error()) + + breakHbProofFunc := func(txn *transactions.SignedTxn) { + txn.Txn.HeartbeatTxnFields.HbProof.Sig[0]++ + } + restoreHbProofFunc := func(txn *transactions.SignedTxn) { + txn.Txn.HeartbeatTxnFields.HbProof.Sig[0]-- + } + // This shows the inner signature must be correct + verifyGroup(t, txnGroups, &blkHdr, breakHbProofFunc, restoreHbProofFunc, crypto.ErrBatchHasFailedSigs.Error()) +} + // TestTxnGroupCacheUpdateFailLogic test makes sure that a payment transaction contains a logic (and no signature) // is valid (and added to the cache) only if logic passes func TestTxnGroupCacheUpdateFailLogic(t *testing.T) { @@ -1028,12 +1093,18 @@ byte base64 5rZMNsevs5sULO+54aN+OvU6lQ503z2X+SSYUABIx7E= verifyGroup(t, txnGroups, &blkHdr, breakSignatureFunc, restoreSignatureFunc, "rejected by logic") } -func createDummyBlockHeader() bookkeeping.BlockHeader { +func createDummyBlockHeader(optVer ...protocol.ConsensusVersion) bookkeeping.BlockHeader { + // Most tests in this file were written to use current. Future is probably + // the better test, but I don't want to make that choice now, so optVer. + proto := protocol.ConsensusCurrentVersion + if len(optVer) > 0 { + proto = optVer[0] + } return bookkeeping.BlockHeader{ Round: 50, GenesisHash: crypto.Hash([]byte{1, 2, 3, 4, 5}), UpgradeState: bookkeeping.UpgradeState{ - CurrentProtocol: protocol.ConsensusCurrentVersion, + CurrentProtocol: proto, }, RewardsState: bookkeeping.RewardsState{ FeeSink: feeSink, @@ -1067,32 +1138,32 @@ func verifyGroup(t *testing.T, txnGroups [][]transactions.SignedTxn, blkHdr *boo breakSig(&txnGroups[0][0]) - dummeyLedger := DummyLedgerForSignature{} - _, err := TxnGroup(txnGroups[0], blkHdr, cache, &dummeyLedger) + dummyLedger := DummyLedgerForSignature{} + _, err := TxnGroup(txnGroups[0], blkHdr, cache, &dummyLedger) require.Error(t, err) require.Contains(t, err.Error(), errorString) // The txns should not be in the cache - unverifiedGroups := cache.GetUnverifiedTransactionGroups(txnGroups[:1], spec, protocol.ConsensusCurrentVersion) + unverifiedGroups := cache.GetUnverifiedTransactionGroups(txnGroups[:1], spec, blkHdr.CurrentProtocol) require.Len(t, unverifiedGroups, 1) - unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups[:2], spec, protocol.ConsensusCurrentVersion) + unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups[:2], spec, blkHdr.CurrentProtocol) require.Len(t, unverifiedGroups, 2) - _, err = TxnGroup(txnGroups[1], blkHdr, cache, &dummeyLedger) + _, err = TxnGroup(txnGroups[1], blkHdr, cache, &dummyLedger) require.NoError(t, err) // Only the second txn should be in the cache - unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups[:2], spec, protocol.ConsensusCurrentVersion) + unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups[:2], spec, blkHdr.CurrentProtocol) require.Len(t, unverifiedGroups, 1) restoreSig(&txnGroups[0][0]) - _, err = TxnGroup(txnGroups[0], blkHdr, cache, &dummeyLedger) + _, err = TxnGroup(txnGroups[0], blkHdr, cache, &dummyLedger) require.NoError(t, err) // Both transactions should be in the cache - unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups[:2], spec, protocol.ConsensusCurrentVersion) + unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups[:2], spec, blkHdr.CurrentProtocol) require.Len(t, unverifiedGroups, 0) cache = MakeVerifiedTransactionCache(1000) @@ -1105,7 +1176,7 @@ func verifyGroup(t *testing.T, txnGroups [][]transactions.SignedTxn, blkHdr *boo // Add them to the cache by verifying them for _, txng := range txnGroups { - _, err = TxnGroup(txng, blkHdr, cache, &dummeyLedger) + _, err = TxnGroup(txng, blkHdr, cache, &dummyLedger) if err != nil { require.Error(t, err) require.Contains(t, err.Error(), errorString) @@ -1115,7 +1186,7 @@ func verifyGroup(t *testing.T, txnGroups [][]transactions.SignedTxn, blkHdr *boo require.Equal(t, 1, numFailed) // Only one transaction should not be in cache - unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups, spec, protocol.ConsensusCurrentVersion) + unverifiedGroups = cache.GetUnverifiedTransactionGroups(txnGroups, spec, blkHdr.CurrentProtocol) require.Len(t, unverifiedGroups, 1) require.Equal(t, unverifiedGroups[0], txnGroups[txgIdx]) diff --git a/data/txntest/txn.go b/data/txntest/txn.go index d734f47576..5c6baaba6b 100644 --- a/data/txntest/txn.go +++ b/data/txntest/txn.go @@ -93,9 +93,11 @@ type Txn struct { StateProof stateproof.StateProof StateProofMsg stateproofmsg.Message - HbAddress basics.Address - HbProof crypto.HeartbeatProof - HbSeed committee.Seed + HbAddress basics.Address + HbProof crypto.HeartbeatProof + HbSeed committee.Seed + HbVoteID crypto.OneTimeSignatureVerifier + HbKeyDilution uint64 } // internalCopy "finishes" a shallow copy done by a simple Go assignment by @@ -287,9 +289,11 @@ func (tx Txn) Txn() transactions.Transaction { Message: tx.StateProofMsg, }, HeartbeatTxnFields: transactions.HeartbeatTxnFields{ - HbAddress: tx.HbAddress, - HbProof: tx.HbProof, - HbSeed: tx.HbSeed, + HbAddress: tx.HbAddress, + HbProof: tx.HbProof, + HbSeed: tx.HbSeed, + HbVoteID: tx.HbVoteID, + HbKeyDilution: tx.HbKeyDilution, }, } } diff --git a/heartbeat/service.go b/heartbeat/service.go index 655fab59a9..d915fa3399 100644 --- a/heartbeat/service.go +++ b/heartbeat/service.go @@ -181,9 +181,11 @@ func (s *Service) prepareHeartbeat(pr account.ParticipationRecordForRound, lates id := basics.OneTimeIDForRound(stxn.Txn.LastValid, pr.KeyDilution) stxn.Txn.HeartbeatTxnFields = transactions.HeartbeatTxnFields{ - HbAddress: pr.Account, - HbProof: pr.Voting.Sign(id, latest.Seed).ToHeartbeatProof(), - HbSeed: latest.Seed, + HbAddress: pr.Account, + HbProof: pr.Voting.Sign(id, latest.Seed).ToHeartbeatProof(), + HbSeed: latest.Seed, + HbVoteID: pr.Voting.OneTimeSignatureVerifier, + HbKeyDilution: pr.KeyDilution, } return stxn diff --git a/heartbeat/service_test.go b/heartbeat/service_test.go index 5ee518616c..bdd06e62df 100644 --- a/heartbeat/service_test.go +++ b/heartbeat/service_test.go @@ -87,7 +87,10 @@ func (l *mockedLedger) WaitMem(r basics.Round) chan struct{} { // BlockHdr allows the service access to consensus values func (l *mockedLedger) BlockHdr(r basics.Round) (bookkeeping.BlockHeader, error) { - if r > l.LastRound() { + l.mu.Lock() + defer l.mu.Unlock() + + if r > l.lastRound() { return bookkeeping.BlockHeader{}, fmt.Errorf("%d is beyond current block (%d)", r, l.LastRound()) } // return the template hdr, with round @@ -96,6 +99,14 @@ func (l *mockedLedger) BlockHdr(r basics.Round) (bookkeeping.BlockHeader, error) return hdr, nil } +// setSeed allows the mock to return a specific seed +func (l *mockedLedger) setSeed(seed committee.Seed) { + l.mu.Lock() + defer l.mu.Unlock() + + l.hdr.Seed = seed +} + func (l *mockedLedger) addBlock(delta table) error { l.mu.Lock() defer l.mu.Unlock() @@ -232,8 +243,8 @@ func TestHeartbeatOnlyWhenChallenged(t *testing.T) { // now we have to make it seem like joe has been challenged. We obtain the // payout rules to find the first challenge round, skip forward to it, then // go forward half a grace period. Only then should the service heartbeat + ledger.setSeed(committee.Seed{0xc8}) // share 5 bits with 0xcc hdr, err := ledger.BlockHdr(ledger.LastRound()) - ledger.hdr.Seed = committee.Seed{0xc8} // share 5 bits with 0xcc a.NoError(err) rules := config.Consensus[hdr.CurrentProtocol].Payouts for ledger.LastRound() < basics.Round(rules.ChallengeInterval+rules.ChallengeGracePeriod/2) { diff --git a/ledger/apply/apply.go b/ledger/apply/apply.go index ecc96c967f..5bbe482f38 100644 --- a/ledger/apply/apply.go +++ b/ledger/apply/apply.go @@ -25,14 +25,14 @@ import ( "github.com/algorand/go-algorand/ledger/ledgercore" ) -// HdrProvider allows fetching old block headers -type HdrProvider interface { +// hdrProvider allows fetching old block headers +type hdrProvider interface { BlockHdr(r basics.Round) (bookkeeping.BlockHeader, error) } // StateProofsApplier allows fetching and updating state-proofs state on the ledger type StateProofsApplier interface { - HdrProvider + hdrProvider GetStateProofNextRound() basics.Round SetStateProofNextRound(rnd basics.Round) GetStateProofVerificationContext(stateProofLastAttestedRound basics.Round) (*ledgercore.StateProofVerificationContext, error) diff --git a/ledger/apply/challenge.go b/ledger/apply/challenge.go index 6dc5fb1a2b..18dadae894 100644 --- a/ledger/apply/challenge.go +++ b/ledger/apply/challenge.go @@ -21,7 +21,6 @@ import ( "github.com/algorand/go-algorand/config" "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/bookkeeping" "github.com/algorand/go-algorand/data/committee" ) @@ -44,12 +43,8 @@ type challenge struct { bits int } -type headerSource interface { - BlockHdr(round basics.Round) (bookkeeping.BlockHeader, error) -} - // FindChallenge returns the Challenge that was last issued if it's in the period requested. -func FindChallenge(rules config.ProposerPayoutRules, current basics.Round, headers headerSource, period ChallengePeriod) challenge { +func FindChallenge(rules config.ProposerPayoutRules, current basics.Round, headers hdrProvider, period ChallengePeriod) challenge { // are challenges active? interval := basics.Round(rules.ChallengeInterval) if rules.ChallengeInterval == 0 || current < interval { diff --git a/ledger/apply/heartbeat.go b/ledger/apply/heartbeat.go index 806c79e2cf..a37c8238a4 100644 --- a/ledger/apply/heartbeat.go +++ b/ledger/apply/heartbeat.go @@ -24,7 +24,7 @@ import ( ) // Heartbeat applies a Heartbeat transaction using the Balances interface. -func Heartbeat(hb transactions.HeartbeatTxnFields, header transactions.Header, balances Balances, provider HdrProvider, round basics.Round) error { +func Heartbeat(hb transactions.HeartbeatTxnFields, header transactions.Header, balances Balances, provider hdrProvider, round basics.Round) error { // Get the account's balance entry account, err := balances.Get(hb.HbAddress, false) if err != nil { @@ -43,18 +43,6 @@ func Heartbeat(hb transactions.HeartbeatTxnFields, header transactions.Header, b kind = "cheap" } - // These first checks are a little draconian. Don't let these free - // transactions do anything except their exact intended purpose. - if len(header.Note) > 0 { - return fmt.Errorf("%s heartbeat is not allowed to have a note", kind) - } - if header.Lease != [32]byte{} { - return fmt.Errorf("%s heartbeat is not allowed to have a lease", kind) - } - if !header.RekeyTo.IsZero() { - return fmt.Errorf("%s heartbeat is not allowed to rekey", kind) - } - if account.Status != basics.Online { return fmt.Errorf("%s heartbeat is not allowed for %s %+v", kind, account.Status, hb.HbAddress) } @@ -70,33 +58,18 @@ func Heartbeat(hb transactions.HeartbeatTxnFields, header transactions.Header, b } } - // Note the contrast with agreement. We are using the account's _current_ - // partkey to verify the heartbeat. This is required because we can only - // look 320 rounds back for voting information. If a heartbeat was delayed a - // few rounds (even 1), we could not ask "what partkey was in effect at - // firstValid-320?" Using the current keys means that an account that - // changes keys would invalidate any heartbeats it has already sent out + // Note the contrast with agreement. We require the account's _current_ + // partkey be used to sign the heartbeat. This is required because we can + // only look 320 rounds back for voting information. If a heartbeat was + // delayed a few rounds (even 1), we could not ask "what partkey was in + // effect at firstValid-320?" Using the current keys means that an account + // that changes keys would invalidate any heartbeats it has already sent out // (that haven't been evaluated yet). Maybe more importantly, after going // offline, an account can no longer heartbeat, since it has no _current_ // keys. Yet it is still expected to vote for 320 rounds. Therefore, // challenges do not apply to accounts that are offline (even if they should // still be voting). - // Conjure up an OnlineAccountData from current state, for convenience of - // oad.KeyDilution(). - oad := basics.OnlineAccountData{ - VotingData: account.VotingData, - } - - sv := oad.VoteID - if sv.IsEmpty() { - return fmt.Errorf("heartbeat address %s has no voting keys", hb.HbAddress) - } - kd := oad.KeyDilution(proto) - - // heartbeats are expected to sign with the partkey for their last-valid round - id := basics.OneTimeIDForRound(header.LastValid, kd) - // heartbeats sign a message consisting of the BlockSeed of the first-valid // round, to discourage unsavory behaviour like presigning a bunch of // heartbeats for later use keeping an unavailable account online. @@ -105,11 +78,16 @@ func Heartbeat(hb transactions.HeartbeatTxnFields, header transactions.Header, b return err } if hdr.Seed != hb.HbSeed { - return fmt.Errorf("provided seed %v does not match round %d's seed %v", hb.HbSeed, header.FirstValid, hdr.Seed) + return fmt.Errorf("provided seed %v does not match round %d's seed %v", + hb.HbSeed, header.FirstValid, hdr.Seed) } - - if !sv.Verify(id, hdr.Seed, hb.HbProof.ToOneTimeSignature()) { - return fmt.Errorf("heartbeat failed verification with VoteID %v", sv) + if account.VotingData.VoteID != hb.HbVoteID { + return fmt.Errorf("provided voter ID %v does not match %v's voter ID %v", + hb.HbVoteID, hb.HbAddress, account.VotingData.VoteID) + } + if account.VotingData.VoteKeyDilution != hb.HbKeyDilution { + return fmt.Errorf("provided key dilution %d does not match %v's key dilution %d", + hb.HbKeyDilution, hb.HbAddress, account.VotingData.VoteKeyDilution) } account.LastHeartbeat = round diff --git a/ledger/apply/heartbeat_test.go b/ledger/apply/heartbeat_test.go index f7d3845b12..6620ad8470 100644 --- a/ledger/apply/heartbeat_test.go +++ b/ledger/apply/heartbeat_test.go @@ -69,12 +69,10 @@ func TestHeartbeat(t *testing.T) { test := txntest.Txn{ Type: protocol.HeartbeatTx, Sender: sender, - Fee: basics.MicroAlgos{Raw: 1}, FirstValid: fv, LastValid: lv, HbAddress: voter, HbProof: otss.Sign(id, seed).ToHeartbeatProof(), - HbSeed: seed, } tx := test.Txn() @@ -82,19 +80,37 @@ func TestHeartbeat(t *testing.T) { rnd := basics.Round(150) // no fee err := Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) - require.ErrorContains(t, err, "cheap heartbeat") + require.ErrorContains(t, err, "free heartbeat") - test.Fee = basics.MicroAlgos{Raw: 10} - tx = test.Txn() // just as bad: cheap + tx.Fee = basics.MicroAlgos{Raw: 10} err = Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) require.ErrorContains(t, err, "cheap heartbeat") - test.Fee = 1000 - tx = test.Txn() + // address fee + tx.Fee = basics.MicroAlgos{Raw: 1000} + + // Seed is missing err = Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) - require.NoError(t, err) + require.ErrorContains(t, err, "provided seed") + tx.HbSeed = seed + // VoterID is missing + err = Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) + require.ErrorContains(t, err, "provided voter ID") + + tx.HbVoteID = otss.OneTimeSignatureVerifier + // still no key dilution + err = Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) + require.ErrorContains(t, err, "provided key dilution 0") + + tx.HbKeyDilution = keyDilution + 1 + err = Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) + require.ErrorContains(t, err, "provided key dilution 778") + + tx.HbKeyDilution = keyDilution + err = Heartbeat(tx.HeartbeatTxnFields, tx.Header, mockBal, mockHdr, rnd) + require.NoError(t, err) after, err := mockBal.Get(voter, false) require.NoError(t, err) require.Equal(t, rnd, after.LastHeartbeat) @@ -129,9 +145,6 @@ func TestCheapRules(t *testing.T) { {1201, 0x01, basics.Online, true, nil, empty, empty, "no challenge"}, // test of the other requirements - {1101, 0x01, basics.Online, true, []byte("note"), empty, empty, "not allowed to have a note"}, - {1101, 0x01, basics.Online, true, nil, [32]byte{'l', 'e', 'a', 's', 'e'}, empty, "not allowed to have a lease"}, - {1101, 0x01, basics.Online, true, nil, empty, [32]byte{'r', 'e', 'k', 'e', 'y'}, "not allowed to rekey"}, {1101, 0xf1, basics.Online, true, nil, empty, empty, "not challenged by"}, {1101, 0x01, basics.Offline, true, nil, empty, empty, "not allowed for Offline"}, {1101, 0x01, basics.Online, false, nil, empty, empty, "not allowed when not IncentiveEligible"}, @@ -168,17 +181,19 @@ func TestCheapRules(t *testing.T) { Seed: seed, }) txn := txntest.Txn{ - Type: protocol.HeartbeatTx, - Sender: sender, - Fee: basics.MicroAlgos{Raw: 1}, - FirstValid: tc.rnd - 10, - LastValid: tc.rnd + 10, - Lease: tc.lease, - Note: tc.note, - RekeyTo: tc.rekey, - HbAddress: voter, - HbProof: otss.Sign(id, seed).ToHeartbeatProof(), - HbSeed: seed, + Type: protocol.HeartbeatTx, + Sender: sender, + Fee: basics.MicroAlgos{Raw: 1}, + FirstValid: tc.rnd - 10, + LastValid: tc.rnd + 10, + Lease: tc.lease, + Note: tc.note, + RekeyTo: tc.rekey, + HbAddress: voter, + HbProof: otss.Sign(id, seed).ToHeartbeatProof(), + HbSeed: seed, + HbVoteID: otss.OneTimeSignatureVerifier, + HbKeyDilution: keyDilution, } tx := txn.Txn() diff --git a/ledger/eval/prefetcher/prefetcher_alignment_test.go b/ledger/eval/prefetcher/prefetcher_alignment_test.go index 6cd6dc9247..ed43a6e7c4 100644 --- a/ledger/eval/prefetcher/prefetcher_alignment_test.go +++ b/ledger/eval/prefetcher/prefetcher_alignment_test.go @@ -1449,7 +1449,8 @@ func TestEvaluatorPrefetcherAlignmentHeartbeat(t *testing.T) { MicroAlgos: basics.MicroAlgos{Raw: 100_000}, }, VotingData: basics.VotingData{ - VoteID: otss.OneTimeSignatureVerifier, + VoteID: otss.OneTimeSignatureVerifier, + VoteKeyDilution: 123, }, }, }, @@ -1463,9 +1464,11 @@ func TestEvaluatorPrefetcherAlignmentHeartbeat(t *testing.T) { Fee: basics.Algos(1), // Heartbeat txn is unusual in that it checks fees a bit. }, HeartbeatTxnFields: transactions.HeartbeatTxnFields{ - HbAddress: makeAddress(2), - HbProof: otss.Sign(firstID, committee.Seed(genesisHash())).ToHeartbeatProof(), - HbSeed: committee.Seed(genesisHash()), + HbAddress: makeAddress(2), + HbProof: otss.Sign(firstID, committee.Seed(genesisHash())).ToHeartbeatProof(), + HbSeed: committee.Seed(genesisHash()), + HbVoteID: otss.OneTimeSignatureVerifier, + HbKeyDilution: 123, }, } diff --git a/ledger/heartbeat_test.go b/ledger/heartbeat_test.go deleted file mode 100644 index 2af0310fef..0000000000 --- a/ledger/heartbeat_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (C) 2019-2024 Algorand, Inc. -// This file is part of go-algorand -// -// go-algorand is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as -// published by the Free Software Foundation, either version 3 of the -// License, or (at your option) any later version. -// -// go-algorand is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with go-algorand. If not, see . - -package ledger - -import ( - "testing" - - "github.com/algorand/go-algorand/config" - "github.com/algorand/go-algorand/crypto" - "github.com/algorand/go-algorand/data/basics" - "github.com/algorand/go-algorand/data/txntest" - ledgertesting "github.com/algorand/go-algorand/ledger/testing" - "github.com/algorand/go-algorand/protocol" - "github.com/algorand/go-algorand/test/partitiontest" - "github.com/stretchr/testify/require" -) - -/* Tests within the `apply` package test the effects of heartbeats, while test - here are closer to integration tests, they test heartbeats in the context of - a more realistic ledger. */ - -// TestHearbeat exercises heartbeat transactions -func TestHeartbeat(t *testing.T) { - partitiontest.PartitionTest(t) - t.Parallel() - - genBalances, addrs, _ := ledgertesting.NewTestGenesis(func(cfg *ledgertesting.GenesisCfg) { - cfg.OnlineCount = 2 // addrs[0] and addrs[1] will be online - }) - heartbeatsBegin := 40 - - ledgertesting.TestConsensusRange(t, heartbeatsBegin, 0, - func(t *testing.T, ver int, cv protocol.ConsensusVersion, cfg config.Local) { - dl := NewDoubleLedger(t, genBalances, cv, cfg) - defer dl.Close() - - dl.txns() // tests involving seed are easier if we have the first block in ledger - - // empty HbAddress means ZeroAddress, and it's not online - dl.txn(&txntest.Txn{Type: "hb", Sender: addrs[1]}, - "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAY5HFKQ has no voting keys") - - // addrs[2] is not online, it has no voting keys - dl.txn(&txntest.Txn{Type: "hb", Sender: addrs[1], HbAddress: addrs[2]}, - addrs[2].String()+" has no voting keys") - - // addrs[1] is online, it has voting keys, but seed is missing - dl.txn(&txntest.Txn{Type: "hb", Sender: addrs[1], HbAddress: addrs[1], FirstValid: 1}, - "does not match round 1's seed") - - // NewTestGenesis creates random VoterID. Verification will fail. - b1, err := dl.generator.BlockHdr(1) - require.NoError(t, err) - dl.txn(&txntest.Txn{ - Type: "hb", - Sender: addrs[1], - HbAddress: addrs[1], - HbSeed: b1.Seed, - FirstValid: 1, - }, - "heartbeat failed verification with") - - // keyreg addr[1] so we have a valid VoterID - const kd = 10 - firstID := basics.OneTimeIDForRound(1, kd) - otss := crypto.GenerateOneTimeSignatureSecrets(firstID.Batch, 5) - dl.txn(&txntest.Txn{ - Type: "keyreg", - Sender: addrs[1], - VotePK: otss.OneTimeSignatureVerifier, - SelectionPK: crypto.VrfPubkey([32]byte{0x01}), // must be non-zero - VoteKeyDilution: kd, - }) - - // Supply and sign the wrong HbSeed - wrong := b1.Seed - wrong[0]++ - dl.txn(&txntest.Txn{ - Type: "hb", - Sender: addrs[1], - HbAddress: addrs[1], - HbSeed: wrong, - HbProof: otss.Sign(firstID, wrong).ToHeartbeatProof(), - FirstValid: 1, - }, - "does not match round 1's seed") - - b2, err := dl.generator.BlockHdr(2) - require.NoError(t, err) - - // Supply the right seed, but sign something else. We're also now - // setting LastValid and the proper OneTimeIDForRound, so that these - // tests are failing for the reasons described, not that. - dl.txn(&txntest.Txn{ - Type: "hb", - LastValid: 30, - Sender: addrs[1], - HbAddress: addrs[1], - HbSeed: b2.Seed, - HbProof: otss.Sign(basics.OneTimeIDForRound(30, kd), wrong).ToHeartbeatProof(), - FirstValid: 2, - }, - "failed verification") - - // Sign the right seed, but supply something else - dl.txn(&txntest.Txn{ - Type: "hb", - LastValid: 30, - Sender: addrs[1], - HbAddress: addrs[1], - HbSeed: wrong, - HbProof: otss.Sign(basics.OneTimeIDForRound(30, kd), b2.Seed).ToHeartbeatProof(), - FirstValid: 2, - }, - "does not match round 2's") - - // Mismatch the last valid and OneTimeIDForRound - dl.txn(&txntest.Txn{ - Type: "hb", - LastValid: 29, - Sender: addrs[1], - HbAddress: addrs[1], - HbSeed: b2.Seed, - HbProof: otss.Sign(basics.OneTimeIDForRound(30, kd), b2.Seed).ToHeartbeatProof(), - FirstValid: 2, - }, - "failed verification") - - // now we can make a real heartbeat, with a properly signed blockseed - dl.txn(&txntest.Txn{ - Type: "hb", - LastValid: 30, - Sender: addrs[1], - HbAddress: addrs[1], - HbSeed: b2.Seed, - HbProof: otss.Sign(basics.OneTimeIDForRound(30, kd), b2.Seed).ToHeartbeatProof(), - FirstValid: 2, - }) - - }) -} diff --git a/test/e2e-go/features/incentives/challenge_test.go b/test/e2e-go/features/incentives/challenge_test.go index 51586eab76..e6c880c611 100644 --- a/test/e2e-go/features/incentives/challenge_test.go +++ b/test/e2e-go/features/incentives/challenge_test.go @@ -71,7 +71,7 @@ func TestChallenges(t *testing.T) { c := fixture.GetLibGoalClientForNamedNode(name) accounts, err := fixture.GetNodeWalletsSortedByBalance(c) a.NoError(err) - a.Len(accounts, 4) + a.Len(accounts, 8) fmt.Printf("Client %s has %v\n", name, accounts) return c, accounts } @@ -149,19 +149,31 @@ func TestChallenges(t *testing.T) { a.True(data.IncentiveEligible) } + // Watch the first half grace period for proposals from challenged nodes, since they won't have to heartbeat. + lucky := util.MakeSet[basics.Address]() + fixture.WithEveryBlock(challengeRound, challengeRound+grace/2, func(block bookkeeping.Block) { + if challenged2.Contains(block.Proposer()) { + lucky.Add(block.Proposer()) + } + }) + // In the second half of the grace period, Node 2 should heartbeat for its accounts beated := util.MakeSet[basics.Address]() fixture.WithEveryBlock(challengeRound+grace/2, challengeRound+grace, func(block bookkeeping.Block) { - for _, txn := range block.Payset { + if challenged2.Contains(block.Proposer()) { + lucky.Add(block.Proposer()) + } + for i, txn := range block.Payset { hb := txn.Txn.HeartbeatTxnFields - fmt.Printf("Heartbeat txn %v\n", hb) + fmt.Printf("Heartbeat txn %v in position %d round %d\n", hb, i, block.Round()) a.True(challenged2.Contains(hb.HbAddress)) // only Node 2 is alive a.False(beated.Contains(hb.HbAddress)) // beat only once beated.Add(hb.HbAddress) + a.False(lucky.Contains(hb.HbAddress)) // we should not see a heartbeat from an account that proposed } a.Empty(block.AbsentParticipationAccounts) // nobody suspended during grace }) - a.Equal(challenged2, beated) + a.Equal(challenged2, util.Union(beated, lucky)) blk, err = fixture.WaitForBlockWithTimeout(challengeRound + grace + 1) a.NoError(err) diff --git a/test/testdata/nettemplates/Challenges.json b/test/testdata/nettemplates/Challenges.json index 6519033e9c..1d9944937c 100644 --- a/test/testdata/nettemplates/Challenges.json +++ b/test/testdata/nettemplates/Challenges.json @@ -4,7 +4,7 @@ "ConsensusProtocol": "future", "LastPartKeyRound": 500, "Wallets": [ - { "Name": "Relay", "Stake": 92, "Online": true }, + { "Name": "Relay", "Stake": 84, "Online": true }, { "Name": "Wallet0", "Stake": 1, "Online": true }, { "Name": "Wallet1", "Stake": 1, "Online": true }, { "Name": "Wallet2", "Stake": 1, "Online": true }, @@ -12,7 +12,15 @@ { "Name": "Wallet4", "Stake": 1, "Online": true }, { "Name": "Wallet5", "Stake": 1, "Online": true }, { "Name": "Wallet6", "Stake": 1, "Online": true }, - { "Name": "Wallet7", "Stake": 1, "Online": true } + { "Name": "Wallet7", "Stake": 1, "Online": true }, + { "Name": "Wallet8", "Stake": 1, "Online": true }, + { "Name": "Wallet9", "Stake": 1, "Online": true }, + { "Name": "WalletA", "Stake": 1, "Online": true }, + { "Name": "WalletB", "Stake": 1, "Online": true }, + { "Name": "WalletC", "Stake": 1, "Online": true }, + { "Name": "WalletD", "Stake": 1, "Online": true }, + { "Name": "WalletE", "Stake": 1, "Online": true }, + { "Name": "WalletF", "Stake": 1, "Online": true } ], "RewardsPoolBalance": 0 }, @@ -28,16 +36,24 @@ { "Name": "Wallet0", "ParticipationOnly": false }, { "Name": "Wallet1", "ParticipationOnly": false }, { "Name": "Wallet2", "ParticipationOnly": false }, - { "Name": "Wallet3", "ParticipationOnly": false } + { "Name": "Wallet3", "ParticipationOnly": false }, + { "Name": "Wallet4", "ParticipationOnly": false }, + { "Name": "Wallet5", "ParticipationOnly": false }, + { "Name": "Wallet6", "ParticipationOnly": false }, + { "Name": "Wallet7", "ParticipationOnly": false } ] }, { "Name": "Node2", "Wallets": [ - { "Name": "Wallet4", "ParticipationOnly": false }, - { "Name": "Wallet5", "ParticipationOnly": false }, - { "Name": "Wallet6", "ParticipationOnly": false }, - { "Name": "Wallet7", "ParticipationOnly": false } + { "Name": "Wallet8", "ParticipationOnly": false }, + { "Name": "Wallet9", "ParticipationOnly": false }, + { "Name": "WalletA", "ParticipationOnly": false }, + { "Name": "WalletB", "ParticipationOnly": false }, + { "Name": "WalletC", "ParticipationOnly": false }, + { "Name": "WalletD", "ParticipationOnly": false }, + { "Name": "WalletE", "ParticipationOnly": false }, + { "Name": "WalletF", "ParticipationOnly": false } ] } ] diff --git a/util/execpool/stream.go b/util/execpool/stream.go index 29ec4613f1..f6017a0af1 100644 --- a/util/execpool/stream.go +++ b/util/execpool/stream.go @@ -87,7 +87,7 @@ func (sv *StreamToBatch) Start(ctx context.Context) { go sv.batchingLoop() } -// WaitForStop waits until the batching loop terminates afer the ctx is canceled +// WaitForStop waits until the batching loop terminates after the ctx is canceled func (sv *StreamToBatch) WaitForStop() { sv.activeLoopWg.Wait() }