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

math/big: add Int.FillBytes #35833

Closed
FiloSottile opened this issue Nov 25, 2019 · 17 comments
Closed

math/big: add Int.FillBytes #35833

FiloSottile opened this issue Nov 25, 2019 · 17 comments
Labels
FrozenDueToAge NeedsFix The path to resolution is known, but the work has not been done. Proposal Proposal-Accepted
Milestone

Comments

@FiloSottile
Copy link
Contributor

When implementing cryptography, what we need is almost always a fixed size buffer representing a value in big endian. What math/big.Int.Bytes provides is a variable size buffer, so all over the place there are snippets of code that do make, Bytes and copy with a len dependent index. I just implemented one the other day for https://golang.org/cl/208484, and just saw one in x/crypto/acme.

I'd be willing to bet that every Bytes invocation in a crypto package is doing something similar. I also learned that random bugs that occur approximately every 256 executions are probably due to missing this step, and I have found such a bug in the wild at least twice.

I propose we solve this at the math/big API level, and add (*math/big.Int).BytesWithSize.

// BytesWithSize returns the absolute value of x as a big-endian
// byte slice of length size. If x doesn't fit in such a buffer,
// BytesWithSize will panic.
func (x *Int) BytesWithSize(size int) []byte {
	b := x.Bytes()
	switch {
	case len(b) > size:
		panic("math/big: value won't fit requested size")
	case len(b) == size:
		return b
	default:
		buf := make([]byte, size)
		copy(buf[size-len(b):], b)
		return buf
	}
}

I don't have a strong opinion on the len(b) > size behavior. Where we use it in crypto packages we always know an upper bound, and if we cross it we might as well panic because something went catastrophically wrong. (And the current code would either panic or silently truncate, which is worse.)

/cc @griesemer @katiehockman

@gopherbot gopherbot added this to the Proposal milestone Nov 25, 2019
@griesemer
Copy link
Contributor

Would it make sense to provide the byte slice to avoid a copy?

func (x *Int) AsBytes(buf []byte)

@robpike
Copy link
Contributor

robpike commented Nov 26, 2019

Yes, I like @griesemer's design. Panic is OK if documented. A nil argument could allocate, perhaps?
That is, if you provide a buffer it must be big enough; otherwise one will be allocated.

@FiloSottile
Copy link
Contributor Author

Oh, I like that!

The nil argument behavior would make sense if we didn't have Bytes, but that's there to stay.

I am not a fan of the fact that one can't really tell which is which based on the names (Bytes vs. AsBytes) but I don't have better suggestions. Maybe BytesWithBuffer? It's a mouthful but hopefully clear? WriteBytes?

@magical
Copy link
Contributor

magical commented Nov 26, 2019

ToFixedBytes? Since the difference from Bytes is that it writes to a fixed-size array (with left padding).

WriteBytes?

The usual name for this would be Read, not Write 😉

@griesemer
Copy link
Contributor

My original thought was also Read, but that name suggests that there's a result (n int, err error) as well which is perhaps overkill, especially if we never expect an error and if we don't care about the n since the buffer will be 0-padded. Also, given a Read method, one would expect a client to be able to call Read repeatedly to get all the bytes of an Int, which is not the case.

Maybe FillBytes if you don't like AsBytes? The former implies more of an "action" (of filling in the bytes) rather than a function call (that returns a value) as does the latter.

@FiloSottile
Copy link
Contributor Author

I like FillBytes, thank you!

// FillBytes sets buf to the absolute value of x in big-endian, zero
// padded as necessary. If x doesn't fit in buf, FillBytes will panic.
func (x *Int) FillBytes(buf []byte)

@robpike
Copy link
Contributor

robpike commented Dec 2, 2019

// FillBytes sets buf to the absolute value of x, storing it as a zero-extended
// big-endian byte stream. If the encoded value doesn't fit in buf, FillBytes will panic.
func (x *Int) FillBytes(buf []byte)

@robpike
Copy link
Contributor

robpike commented Dec 2, 2019

I'm a little unsettled by the absolute value thing, but that's what Bytes does.

@FiloSottile
Copy link
Contributor Author

I'm a little unsettled by the absolute value thing, but that's what Bytes does.

Same, but on the other hand every way of encoding negative numbers I know of went terribly wrong, so I see where that choice is coming from.

@rsc rsc changed the title proposal: math/big: add Int.BytesWithSize proposal: math/big: add Int.FillBytes Dec 11, 2019
@rsc
Copy link
Contributor

rsc commented Dec 11, 2019

Based on the discussion, this sounds like a likely accept.

Leaving open for a week for final comments.

@rsc
Copy link
Contributor

rsc commented Jan 8, 2020

No final comments, so accepted.

@rsc rsc changed the title proposal: math/big: add Int.FillBytes math/big: add Int.FillBytes Jan 8, 2020
@rsc rsc modified the milestones: Proposal, Backlog Jan 8, 2020
@FiloSottile FiloSottile modified the milestones: Backlog, Go1.15 Jan 8, 2020
@FiloSottile FiloSottile added the NeedsFix The path to resolution is known, but the work has not been done. label Feb 7, 2020
@gopherbot
Copy link
Contributor

Change https://golang.org/cl/230397 mentions this issue: math/big: add (*Int).FillBytes

@FiloSottile
Copy link
Contributor Author

Implemented this in CL 230397.

Note that I made FillBytes return the buffer, because it was nicer to use in all call sites I updated. Let me know if there are objections to that.

ceseo pushed a commit to ceseo/go that referenced this issue May 5, 2020
Replaced almost every use of Bytes with FillBytes.

Note that the approved proposal was for

    func (*Int) FillBytes(buf []byte)

while this implements

    func (*Int) FillBytes(buf []byte) []byte

because the latter was far nicer to use in all callsites.

Fixes golang#35833

Change-Id: Ia912df123e5d79b763845312ea3d9a8051343c0a
Reviewed-on: https://go-review.googlesource.com/c/go/+/230397
Reviewed-by: Robert Griesemer <gri@golang.org>
@FiloSottile
Copy link
Contributor Author

Fixed by c9d5f60, which for some reason didn't close this.

xujianhai666 pushed a commit to xujianhai666/go-1 that referenced this issue May 21, 2020
Replaced almost every use of Bytes with FillBytes.

Note that the approved proposal was for

    func (*Int) FillBytes(buf []byte)

while this implements

    func (*Int) FillBytes(buf []byte) []byte

because the latter was far nicer to use in all callsites.

Fixes golang#35833

Change-Id: Ia912df123e5d79b763845312ea3d9a8051343c0a
Reviewed-on: https://go-review.googlesource.com/c/go/+/230397
Reviewed-by: Robert Griesemer <gri@golang.org>
@lukechampine
Copy link
Contributor

Sorry for not commenting earlier, but can I ask why an append-style API wasn't used here? If all I saw was the signature:

func (*Int) FillBytes(buf []byte) []byte

I would assume that it behaved like (hash.Hash).Sum (and friends), i.e. that FillBytes(nil) was equivalent to Bytes(). Given that it returns a slice, I certainly wouldn't expect it to panic.

I don't think we can change the behavior now, unfortunately, but maybe an AppendBytes is warranted...?

@FiloSottile
Copy link
Contributor Author

@lukechampine What this API was specifically addressing, more than the performance advantage of saving an allocation, was the need to fill a fixed-size buffer with a possibly shorter big.Int, because big.Ints don't have an innate size. There is no way to do that with an append-like API.

@lukechampine
Copy link
Contributor

Ah, because of the big-endianness, right. Thanks for the clarification.

xc added a commit to digimakergo/digimaker that referenced this issue Aug 2, 2021
@golang golang locked and limited conversation to collaborators Sep 29, 2021
@rsc rsc moved this to Accepted in Proposals Aug 10, 2022
@rsc rsc added this to Proposals Aug 10, 2022
@rsc rsc removed this from Proposals Oct 19, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsFix The path to resolution is known, but the work has not been done. Proposal Proposal-Accepted
Projects
None yet
Development

No branches or pull requests

8 participants