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

Distributed verification #256

Merged
merged 10 commits into from
Jan 31, 2024
Merged

Distributed verification #256

merged 10 commits into from
Jan 31, 2024

Conversation

poszu
Copy link
Collaborator

@poszu poszu commented Dec 22, 2023

Changes to support distributed verification (spacemeshos/go-spacemesh#5185):

  • added verification modes (All, One, and Subset)
  • Subset takes the number of indices to verify and a seed
    • the seed is used to initialize local randomness for selecting K3 indices (so that nodes verify different subsets)
  • return the invalid index in POST proof - required for malfeasance proof

💡 There are some changes in C definitions. It's caused by improved enum definitions in the FFI on the post-rs side.

@poszu poszu requested review from dshulyak and fasmat December 22, 2023 09:09
@poszu poszu force-pushed the distributed-post-verification branch from b61fcc7 to 7a7c884 Compare December 22, 2023 09:10
Copy link

codecov bot commented Dec 22, 2023

Codecov Report

Attention: 11 lines in your changes are missing coverage. Please review.

Comparison is base (a09cf87) 70.7% compared to head (f559650) 71.4%.

Files Patch % Lines
internal/postrs/initializer.go 42.8% 4 Missing ⚠️
internal/postrs/proof.go 93.4% 3 Missing and 1 partial ⚠️
internal/postrs/pos_verifier.go 72.7% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff            @@
##           develop    #256     +/-   ##
=========================================
+ Coverage     70.7%   71.4%   +0.6%     
=========================================
  Files           29      29             
  Lines         1816    1879     +63     
=========================================
+ Hits          1285    1342     +57     
- Misses         391     396      +5     
- Partials       140     141      +1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@poszu poszu force-pushed the distributed-post-verification branch from 7a7c884 to 11eb06b Compare December 22, 2023 09:15
@poszu poszu force-pushed the distributed-post-verification branch from b96bdb6 to df4c17a Compare December 22, 2023 14:30
@poszu poszu force-pushed the distributed-post-verification branch from dd8f476 to 8598aa7 Compare December 28, 2023 10:02
@poszu poszu marked this pull request as ready for review January 8, 2024 15:23
Copy link
Member

@fasmat fasmat left a comment

Choose a reason for hiding this comment

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

I'm not a fan of the proposed ABI for post-rs. I think it would be much simpler to just have 3 verify functions on the ABI level:

  • C.verifyAll(...)
  • C.verifyOne(index, ...)
  • C.verifySubset(k3, seed, ...)

Outside of the internal package we would still have only one Verify method with functional options that just depending on the provided options calls a different C function. On the rust side I'm sure that the 3 exported functions can just call one general verify function with different parameters depending on the ABI function that was called. This makes the C interface much simpler, no need to serialize objects / structs across ABI if we can do it with simple types.

With arrays I see it similarly:

cSeed := C.CBytes(seed)
defer C.free(cSeed)

// len(seed) is arguably not needed because we can already check on the Go side that it is 32 bytes
// and return an error if it is not
C.verify(..., cSeed, len(seed), ...)

seems easier and is safer than:

cSeed := C.ArrayU8{
	ptr: (*C.uchar)(unsafe.SliceData(seed)),
	len: C.size_t(len(seed)),
	cap: C.size_t(cap(seed)),
}
C.verify(..., cSeed, ...)

I'm not even sure that the 2nd one is necessarily faster than the first one 🤔

Comment on lines +77 to +79
case C.VerifyResult_Invalid:
result := castBytes[C.VerifyPosResult_Invalid_Body](result.anon0[:])
return fmt.Errorf("%w: file %d contains invalid label at offset %d", ErrInvalidPos, result.file, result.offset)
Copy link
Member

@fasmat fasmat Jan 22, 2024

Choose a reason for hiding this comment

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

That seems a) dangerous and b) very platform / compiler specific; i.e. just because it works on windows/amd64 doesn't mean it works on macOS/arm64. I think it would be simpler and safer to just return "file n contains invalid label at offset x" as string from post-rs and then wrap it in the returned error:

Suggested change
case C.VerifyResult_Invalid:
result := castBytes[C.VerifyPosResult_Invalid_Body](result.anon0[:])
return fmt.Errorf("%w: file %d contains invalid label at offset %d", ErrInvalidPos, result.file, result.offset)
case C.VerifyResult_Invalid:
return fmt.Errorf("%w: %s", ErrInvalidPos, C.GoString(result.error))

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

All C code is dangerous 🙃. What you propose works for verifying POS, but wouldn't work for verifying proof - we need the index there.

Copy link
Member

@fasmat fasmat Jan 24, 2024

Choose a reason for hiding this comment

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

I think the issue I have with this approach is that the return value of the verify function is essentially a void * (or at least of the field anon0 in the returned object), but I don't think it needs to be. Neither here nor in the case of verifying proof. The return value for C.verify_pos can just be (in go syntax):

type pos_result struct {
	tag    int    // the error code
	file   string // only set when tag == C.VerifyResult_Invalid
	offset int    // only set when tag == C.VerifyResult_Invalid
}

and the return value for C.verify_proof can instead be:

type proof_result struct {
	tag      int // the error code
	index_id int // only set when tag == C.VerifyResult_InvalidIndex
}

Without a generic approach the object-byte-cast wouldn't be necessary 🙂.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can just give the anon0 field a name? Then I'd already be happy 🙂

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The header is autogenerated with https://github.com/mozilla/cbindgen. I looked but I didn't find a way to force it to name the union :(

internal/postrs/proof.go Outdated Show resolved Hide resolved
internal/postrs/proof.go Outdated Show resolved Hide resolved
internal/postrs/proof.go Outdated Show resolved Hide resolved
proving/proving_options.go Show resolved Hide resolved
proving/proving_test.go Show resolved Hide resolved
@poszu
Copy link
Collaborator Author

poszu commented Jan 23, 2024

I'm not a fan of the proposed ABI for post-rs. I think it would be much simpler to just have 3 verify functions on the ABI level:

  • C.verifyAll(...)
  • C.verifyOne(index, ...)
  • C.verifySubset(k3, seed, ...)

Outside of the internal package we would still have only one Verify method with functional options that just depending on the provided options calls a different C function. On the rust side I'm sure that the 3 exported functions can just call one general verify function with different parameters depending on the ABI function that was called. This makes the C interface much simpler, no need to serialize objects / structs across ABI if we can do it with simple types.

Sure, it's possible to create 3 functions instead. I'm not sure if that's simpler, you trade 1 complexity for another.

With arrays I see it similarly:

cSeed := C.CBytes(seed)
defer C.free(cSeed)

// len(seed) is arguably not needed because we can already check on the Go side that it is 32 bytes
// and return an error if it is not
C.verify(..., cSeed, len(seed), ...)

seems easier and is safer than:

cSeed := C.ArrayU8{
	ptr: (*C.uchar)(unsafe.SliceData(seed)),
	len: C.size_t(len(seed)),
	cap: C.size_t(cap(seed)),
}
C.verify(..., cSeed, ...)

I'm not even sure that the 2nd one is necessarily faster than the first one 🤔

Avoiding a copy would be faster, but ArrayU8 is not about performance. Originally, it was required to be able to pass an object allocated on the Rust side to Go and to be able to free it afterward (hence it must contain cap). It's not required to pass data from Go to Rust - it was used as a convenience as the type was already there.

Additionally, in situations when it is needed to pass 2 slices, it's better to pass 2 objects that contain ptr + len rather than ptr0, len0, ptr1, len1 as the former is less error-prone.

@poszu poszu enabled auto-merge January 31, 2024 14:11
@poszu poszu merged commit 7925759 into develop Jan 31, 2024
10 checks passed
@poszu poszu deleted the distributed-post-verification branch January 31, 2024 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants