Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

network: Limit message length based on Tag #5388

Merged
merged 64 commits into from
Jun 23, 2023

Conversation

iansuvak
Copy link
Contributor

@iansuvak iansuvak commented May 15, 2023

Summary

This PR implements specific message length limits based on the protocol Tag of the message being currently read in the wsPeer.

The new <TypeName>MaxSize() int methods are being generated by the msgp library based on the supplied allocbounds and the new struct annotation totalallocbound used to limit the total number of bytes encoded in a collection type.

New constants are defined and Tag.MaxSize() method switching between them is used when reseting LimitedReaderSlurper. These don't override the global maxAllocation setting so if max message size is larger than the maxAllocation we will still error out with the old limit.

TODOs:

  • Fix allocbounds/totalallocbounds on StateProofs that are currently blowing up the maxsize of transactions.
  • Write a method calculating maxSize for TS TopicMsgRes tags We decided to keep TS and VB at existing maximum message size since their theoretical limits weren't meaningfully different.
  • Implement actual enforcement of limits on topic messages (MI, TS, UE) since those are not read using LimitedReadSlurper This statement was false. They are read using the slurper.

Test Plan

  • Add new tests to make sure that constants are defined for each Tag
  • Add tests to make sure that custom calculated constants are correct
  • Write new tests for the slurper to make sure that the per message limits work

@iansuvak iansuvak marked this pull request as draft May 15, 2023 21:36
catchup/universalFetcher_test.go Show resolved Hide resolved
agreement/types.go Outdated Show resolved Hide resolved
go.mod Outdated Show resolved Hide resolved
go.sum Outdated Show resolved Hide resolved
network/limited_reader_slurper.go Outdated Show resolved Hide resolved
node/node_test.go Show resolved Hide resolved
protocol/codec_tester.go Show resolved Hide resolved
network/wsNetwork.go Outdated Show resolved Hide resolved
protocol/txntype.go Show resolved Hide resolved
data/transactions/signedtxn.go Outdated Show resolved Hide resolved
data/transactions/teal.go Outdated Show resolved Hide resolved
protocol/tags.go Outdated Show resolved Hide resolved
iansuvak and others added 4 commits June 7, 2023 11:42
// proof size for individual message types we have smaller valid bounds.
// Exported because it's used in the stateproof package for ensuring that SigPartProof constant is correct size.
func ProofMaxSizeByElements(n int) (s int) {
s = 1 + 4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am very worried about having such fine-grained calculations done manually.
The tradeoff we need to consider is:
what is the risk of allowing loose bounds versus having such fine-grained calculations done manually.

Still considering this, and making sure the calculations are correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's actually not fully manual. It's a modification of the autogenerated test: here https://github.com/algorand/go-algorand/pull/5388/files#diff-add6a5ef46c2629eb7484bf3113df670d70cfdb6661ca69e236a778a7e1cb4c4R334

and we have a test here https://github.com/algorand/go-algorand/pull/5388/files#diff-181a1af1a38a976d2ddce9a94f778520e2ad787079c3cc99dbd1b7483386aa73R248 that ensures that that auto-generated ProofMaxSize and and ProofMaxSizeByElements output the same value when the allocbound is used as the input

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is very good and reliable. However, the mapping of the function SingleLeafProofMaxSize to SingleLeafProof is not obvious for someone who is not aware of this arrangement.

In this particular case, this function is not needed. It can be implemented by setting a constant bound directly to the Proof field in:

        Signature struct {
                _struct struct{} `codec:",omitempty,omitemptyarray"`

                Signature             crypto.FalconSignature      `codec:"sig"`
                VectorCommitmentIndex uint64                      `codec:"idx"`
                Proof                 merklearray.SingleLeafProof `codec:"prf"`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but we do have a pattern for doing this manual implementation already for basics.MicroAlgos and this solution will continue working if we ever use SingleLeafProof elsewhere. Finding it's maxtotalbytes bound and using it is more implicit IMO.

crypto/merklearray/proof.go Outdated Show resolved Hide resolved
crypto/merklearray/proof.go Outdated Show resolved Hide resolved
crypto/merklearray/proof.go Outdated Show resolved Hide resolved
// proof size for individual message types we have smaller valid bounds.
// Exported because it's used in the stateproof package for ensuring that SigPartProof constant is correct size.
func ProofMaxSizeByElements(n int) (s int) {
s = 1 + 4
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test is very good and reliable. However, the mapping of the function SingleLeafProofMaxSize to SingleLeafProof is not obvious for someone who is not aware of this arrangement.

In this particular case, this function is not needed. It can be implemented by setting a constant bound directly to the Proof field in:

        Signature struct {
                _struct struct{} `codec:",omitempty,omitemptyarray"`

                Signature             crypto.FalconSignature      `codec:"sig"`
                VectorCommitmentIndex uint64                      `codec:"idx"`
                Proof                 merklearray.SingleLeafProof `codec:"prf"`

Comment on lines +45 to +48
// VRFVerifierMaxSize forwards to base implementation since it's expected by the msgp generated MaxSize functions
func VRFVerifierMaxSize() int {
return VrfPubkeyMaxSize()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? The bound can be set like elsewhere in the struct field where this is used.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because of type alias. This type is on TODO for removal.


// MessageOfInterestMaxSize returns the maximum size of a MI message sent over the network
// by encoding all of the tags currenttly in use.
func MessageOfInterestMaxSize() int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be in the test file, since it is only used from the test.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move it but would have to move it to node_test.go which seems like an odd place for it and it would break symmetry since all other MaxSize() functions are available in non-test code.

@@ -108,6 +108,12 @@ func (a MicroAlgos) MsgIsZero() bool {
return a.Raw == 0
}

// MicroAlgosMaxSize returns maximum possible msgp encoded size of MicroAlgos in bytes.
// It is expected by msgp generated MaxSize functions
func MicroAlgosMaxSize() (s int) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? MicroAlgos has one field of type uint64.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same reason as the previous conversations we've had: msgp when it sees a type defined outside of the current file it's parsing it assumes that it has this function defined. MicroAlgos is an example of a struct that we generate fully manually and don't use msgp for but we still need this method and the ones above for the rest of the msgp generated functions to work since they call them when they are Marshalling/Unmarshalling themselves or checking their own MaxSize

@iansuvak iansuvak marked this pull request as ready for review June 22, 2023 21:43
@iansuvak iansuvak changed the title WIP: network: Limit message length based on Tag network: Limit message length based on Tag Jun 22, 2023
protocol/tags_test.go Outdated Show resolved Hide resolved
Comment on lines +45 to +48
// VRFVerifierMaxSize forwards to base implementation since it's expected by the msgp generated MaxSize functions
func VRFVerifierMaxSize() int {
return VrfPubkeyMaxSize()
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because of type alias. This type is on TODO for removal.

algorandskiy
algorandskiy previously approved these changes Jun 23, 2023
Copy link
Contributor

@AlgoAxel AlgoAxel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed primarily the Network and Tags modules, and looked lightly at the rest.

@@ -100,7 +100,7 @@ func RandomAssetParams() basics.AssetParams {
DefaultFrozen: crypto.RandUint64()%2 == 0,
}
if crypto.RandUint64()%5 != 0 {
ap.UnitName = fmt.Sprintf("un%x", uint32(crypto.RandUint64()%0x7fffffff))
ap.UnitName = fmt.Sprintf("un%x", uint32(crypto.RandUint64()%0x7fffff))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you did not write the conditional, but do you happen to know why we check if the random int is divisible by 5?

network/wsPeer.go Show resolved Hide resolved
Comment on lines 270 to 276
for _, spec := range genDecl.Specs {
if valueSpec, ok := spec.(*ast.ValueSpec); ok {
for _, n := range valueSpec.Names {
if n.Name == "TagLength" {
if strings.HasSuffix(n.Name, "MaxSize") || n.Name == "TagLength" {
continue
}
declaredTags = append(declaredTags, n.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if I'm reading this block right, we're trying to iterate over the valueSpec of a given tag, and through excluding MaxSize and TagLength, we expect that the only remaining thing is the declared tag's name? This seems fragile if/when more is added to the spec. Since it's just a test I don't know that it's worth pursuing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it is fragile as you can see since both myself and Shant had to add exclusions here for MaxSize and TagLength respectively. Hopefully we won't be adding a lot more specs to protocol.Tags

protocol/tags.go Show resolved Hide resolved
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants