Introduce and use a new compact_size module#3259
Conversation
|
I don't think we should have a type for this. We decided elsewhere that it should be a method as part of the encoding/decoding traits. And interesting re |
We need it in |
|
How is the usage in If we are just trying to match the existing code to minimize the diff, then we shouldn't do the rename. |
|
Oh I follow you, the same logic but just use stand alone functions instead of creating a type. I can try that and see how it looks. |
|
Exactly, the type is basically over-engineering. I suggest putting these methods to internals: fn encode_compact_size(num: impl ToU64) -> ArrayVec<u8> { /* ... */ }
fn decode_compact_size(bytes: &mut &[u8]) -> Result<u64, () /* too short */> { /* ... */ }
fn encoded_size_of_compact_size(num: impl ToU64) -> usize { /* ... */ } // technically could be `u8` but that'll likely be annoying. |
There was a problem hiding this comment.
Just make this ArrayVec in internals.
There was a problem hiding this comment.
I removed this function and returned a [8u; 9].
1b86b5b to
8ca250d
Compare
|
I had hacked up a new solution before seeing your review @Kixunil, I'm EOD today, I'll take another look tomorrow. |
CompactSize typecompact_size module
8ca250d to
0b20ee8
Compare
0b20ee8 to
014ac3d
Compare
|
Note in this PR |
|
In case its not obvious, this is needed for the work on |
| /// assert_eq!(size, 1); | ||
| /// assert_eq!(encoded, [0x20, 0, 0, 0, 0, 0, 0, 0, 0]); | ||
| /// ``` | ||
| pub fn encode(value: u64) -> ([u8; 9], usize) { |
There was a problem hiding this comment.
No used, found a bug too - so WIN.
EDIT: Now used.
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub enum DecodeError { | ||
| /// Tried to decode a `CompactSize` from an empty slice. | ||
| Empty, |
There was a problem hiding this comment.
Empty is literally an instance of TooShort, we don't need it.
There was a problem hiding this comment.
Ha! Conceptually yes but we don't have the expected length until we read the first byte.
(I removed Empty at one stage to find this out :)
There was a problem hiding this comment.
Empty means we have less than one. Having less than one means empty.
There was a problem hiding this comment.
To make Kix's point more explicit, the "expected minimum length" should be 1.
As for whether we should combine the two variants, I could go either way. I guess the fewer variants the better, especially here where we only have two variants and we could eliminate the whole enum.
There was a problem hiding this comment.
"expected minimum length" should be 1.
Ah, adding "minimum" indeed solves the problem.
There was a problem hiding this comment.
Also FTR we could've just wrapped the error.
There was a problem hiding this comment.
I'd prefer
Result<_, ()>toOption.
To imply its an error case but we don't provide a specific error?
There was a problem hiding this comment.
Yes, also triggers the must_use warning.
There was a problem hiding this comment.
Sorry to pile on, but I'd prefer we use a dummy error type which we mark #[non_exhaustive] so users can't construct it. This will give us the freedom to add information to the error variant in the future.
There was a problem hiding this comment.
internals is unstable anyway, but I'm not opposed to it.
| #[derive(Debug, Clone, PartialEq, Eq)] | ||
| pub struct TooShortError { | ||
| initial_byte: u8, | ||
| expected_minimum_length: usize, |
There was a problem hiding this comment.
These are redundant since you can compute one from the other.
There was a problem hiding this comment.
Who cares? There is no perf hit in having an extra field in an error type, right? Its more brain-dead-simple to carry the redundant info than having more byte -> length conversions.
There was a problem hiding this comment.
Technically, there actually is. Size of error types causes the whole Result to be larger if it's larger than the Ok variant and this slows down Result operations downstream. We generally don't pay too much attention to this since bad errors are worse than bad performance in most cases but if we can reasonably compress the information we should. The return type of the function is u64 which makes your Result ~twice as large as it would be if it was just the value itself. However it'd be really strange if this simple function didn't get inlined which may remove the cost entirely.
And really, the information we need can fit just 4 bits (see below). But I'm not asking you to implement that madness, I'm only asking to fit it into ~u64. :)
Side note: it annoys me that ArrayVec has to use usize for length tracking even if CAP is small enough to fit u8.
4 bits because the errors that can possibly exist are only these:
- The input is empty
- The input encodes two-byte requirement and one of the bytes is missing
- The input encodes four-byte requirement and at most four bytes are missing - four combinations
- The input encodes eight-byte requirement and at most eight bytes are missing - eight combinations
Which gives us 1 + 1 + 4 + 8 == 14 combination which can be encoded by 4 bits.
Yes, it was a fun puzzle to find the true lower bound and not just any solution that fits into a byte. :)
BTW the most efficient way you can write it in Rust is to enumerate all the possibilities as enum because that also gives niche optimizations.
There was a problem hiding this comment.
I didn't read Kix's crazy analysis, but definitely +1 to minimizing error type sizes where it's reasonably possible.
In miniscript we have a massive error types everywhere and it's a big problem for us. It's not so big a problem here but we shouldn't get complacent.
There was a problem hiding this comment.
Cool, thanks for the explanation. TIL.
There was a problem hiding this comment.
In
miniscriptwe have a massive error types everywhere and it's a big problem for us. It's not so big a problem here but we shouldn't get complacent.
TIL about the result_large_err clippy lint, which fires on errors that are over 128 bytes, but which can be configured to fire at a lower threshold.
| /// let (compact, _) = compact_size::decode(&[0x20, 0xAB, 0xCD]).unwrap(); | ||
| /// assert_eq!(compact, 32); | ||
| /// ``` | ||
| pub fn decode(slice: &[u8]) -> Result<(u64, usize), DecodeError> { |
There was a problem hiding this comment.
Rather than returning usize and forcing the callers to recompute it's better to take slice as &mut &[u8] and mutate it to update the start of the slice. You can then avoid a bunch of redundant code.
There was a problem hiding this comment.
I'll take a look, thanks.
There was a problem hiding this comment.
With fresh eyes I still don't understand what you mean, can you extrapolate please?
There was a problem hiding this comment.
This signature is better:
pub fn decode(slice: &mut &[u8]) -> Result<u64, DecodeError>
Where the function updates the slice by moving start by the number of consumed bytes.
Because callers always want to update their positions in the slice, this method is more helpful and doing it manually is annoying and error-prone.
There was a problem hiding this comment.
I thought that was what you might be meaning but in both callsites in this PR the caller does not update the slice.
There was a problem hiding this comment.
Oh you are right, the iterator does update it. The other callsite (in element_at), does not and does not have a mutable reference to it ether (called from nth). Also decode_cursor does not mutate the slice, I still rekon we should leave it as is. Thoughts?
There was a problem hiding this comment.
All of them mutate the slice in the sense that they re-construct the new one in the exact same way - to start after the compact size bytes. They could as well make a mut bytes copy and mutate it. You could argue we should return (u64, &[u8]) to keep the style functional which is not terrible either, I'm just used to mutating style because of deserializers. Also I'm not sure what the borrow checker will think of it although it's likely fine.
|
Thanks for the review @Kixunil , I"ll re-spin tomorrow. |
014ac3d to
d932736
Compare
|
Now uses |
|
Another question: While rebasing Steven's work in #2931 on top of this I see that we do checks or non-minimal encoding when decoding a compact size slice, should we be doing that here? Non-minimal means encoding a value that fits in a fn read_compact_size(&mut self) -> Result<u64, Error> {
match self.read_u8()? {
0xFF => {
let x = self.read_u64()?;
if x < 0x1_0000_0000 {
Err(Error::NonMinimalVarInt)
} else {
Ok(x)
}
}
0xFE => {
let x = self.read_u32()?;
if x < 0x1_0000 {
Err(Error::NonMinimalVarInt)
} else {
Ok(x as u64)
}
}
0xFD => {
let x = self.read_u16()?;
if x < 0xFD {
Err(Error::NonMinimalVarInt)
} else {
Ok(x as u64)
}
}
n => Ok(n as u64),
}
} |
|
Yes. In Core these checks are done unconditionally. |
|
I'd rather remove non-minimality check entirely. It doesn't really matter for |
|
OMG, we are really going around in circles. I can remove the checks but please note that above we agreed that we should have them (see #3259 (comment)). |
|
And they are unconditionally check in Core template<typename Stream>
uint64_t ReadCompactSize(Stream& is, bool range_check = true)
{
uint8_t chSize = ser_readdata8(is);
uint64_t nSizeRet = 0;
if (chSize < 253)
{
nSizeRet = chSize;
}
else if (chSize == 253)
{
nSizeRet = ser_readdata16(is);
if (nSizeRet < 253)
throw std::ios_base::failure("non-canonical ReadCompactSize()");
}
else if (chSize == 254)
{
nSizeRet = ser_readdata32(is);
if (nSizeRet < 0x10000u)
throw std::ios_base::failure("non-canonical ReadCompactSize()");
}
else
{
nSizeRet = ser_readdata64(is);
if (nSizeRet < 0x100000000ULL)
throw std::ios_base::failure("non-canonical ReadCompactSize()");
}
if (range_check && nSizeRet > MAX_SIZE) {
throw std::ios_base::failure("ReadCompactSize(): size too large");
}
return nSizeRet;
} |
494d71a to
ac04b1c
Compare
|
@tcharding sure, the checks need to pass -- but there shouldn't be any possible way for them not to pass. This is why a |
ac04b1c to
b01a977
Compare
|
Added calls to |
b01a977 to
b9706fc
Compare
|
Must be time for lunch, I had |
|
@tcharding we agreed to those checks because we didn't notice the functions are not taking outside inputs. Now that we've realized that it's obvious that the checks are just wasted performance. I guess this should be explicitly documented on the functions. |
|
Can we merge as is and follow up now? Docs and things is no bother to add later. |
There was a problem hiding this comment.
The commit message is stale.
There was a problem hiding this comment.
Could've been ToU64 to simplify the callers.
There was a problem hiding this comment.
Ha! Can't call that in const, sadly I had to make the change before I remembered why I didn't use it originally :)
There was a problem hiding this comment.
const trait methods would be awesome.
There was a problem hiding this comment.
I just removed the const - we will likely need to add it again later but in this PR its better as you suggest.
|
utACK b9706fca04ed1aee7bc4db91c4868ef7449000eb. This is fine for internal code. In a later rev we should:
Also +1 to Kix's comments above -- the commit message refers to |
That wouldn't reduce noise just make debugging harder. But enums are better anyway.
Actually it already panics for values larger than
Definitely before 1.0 release and ideally also before any other release. |
We would like to move the witness module to `primitives` but there is a bunch of usage of `VarInt`. Introduce a module that does the encoding and decoding instead, this code is internal so put it in `internals`. Note we add an unused public `MAX_ENCODABLE_SIZE` variable that is commented with an issue link. Done like this because its quite important that we see to it and it makes it clear that we are not and we know about it. rust-bitcoin#3264
b9706fc to
d65de7c
Compare
|
The force push is:
I checked the range-diff and its easy enough to review. Thanks for your patience on this one, its been trying :) |
|
Bump please, this guy is P-high |
compact_size modulecompact_size module
|
ooooo nice, I hope your merge script has finished running by the time I'm done the school run! |
|
Lol, it had finished but I forgot to type "push". |
|
Yesterdays |
compact_size modulecompact_size module
We would like to move the witness module to
primitivesbut there is a bunch of usage ofVarInt.Introduce a module that does the encoding and decoding instead, note that while the functionality is internal decoding returns an error which may one day end up in the public API. So put the module in
primitivesand make it public.Adds the module to
primitives, adds a publicMAX_ENCODABLE_SIZEvariable that is commented with an issue link.#3264