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

Validate proposals on prepare and submit #1488

Merged
merged 3 commits into from
Jun 26, 2017

Conversation

tgflynn
Copy link

@tgflynn tgflynn commented Jun 6, 2017

There have been some issues with the submission of invalid proposals. Currently these are detected only by sentinel and can thus result in the loss of the collateral fee for the submitter.

This PR replicates sentinel's proposal validation in dashcore and will reject invalid proposals from being prepared or submitted.

It also provides a new RPC command: "gobject check" which allows proposal data to be validated without attempting to create a proposal.

Tim Flynn added 2 commits June 5, 2017 21:35
Includes commits:
  Implemented CProposalValidator

  Use CProposalValidator to check proposals at prepare and submit stages

  Modify proposal validator to support numerical data in string format

  Multiple bug fixes in governance-validators.cpp

  Fixed bug in CheckURL

  Fixed stream state check

  Increase strictness of payment address validation for compatibility with sentinel

  Improved error reporting

  Implemented "check" rpc command to validate proposals

  Fixes to RPC check command

  Fix error message

  Unit test and data files for proposal validator

  Added test cases

  Removed debugging code
Copy link

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

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

Nice, thanks 👍
Looks mostly ok for me, see inline comments for few issues.

bool CProposalValidator::Validate()
{
if(!ValidateJSON()) {
return false;
Copy link

Choose a reason for hiding this comment

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

Maybe add error msg like "Invalid JSON" or "Invalid format" here to avoid silent failure

Copy link
Author

Choose a reason for hiding this comment

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

Those error messages are added by ParseJSONData. The only case not covered is if the data string is empty. I will add a message for that.

}

if(nEndEpoch <= nStartEpoch) {
strErrorMessages += "end_epoch <= start_epoch field not found;";
Copy link

Choose a reason for hiding this comment

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

This message is confusing

Copy link
Author

Choose a reason for hiding this comment

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

Yes, that's a bug, will fix.

}

if(dValue <= 0.0) {
strErrorMessages += "payment_amount invalid;";
Copy link

Choose a reason for hiding this comment

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

Could be more specific here i.e. "payment_amount is negative". Also shouldn't there also be a max payment_amount validation (i.e. if (dValue > total_budget_at_end_epoch) ...)?

Copy link
Author

Choose a reason for hiding this comment

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

My goal was to replicate the sentinel Proposal.is_valid() method.

However though that method doesn't check for too large amounts, such amounts will be rejected at superblock trigger creation time so I guess it would make sense to add that check here.

Copy link

Choose a reason for hiding this comment

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

Also Validate() concatenates "Invalid payment amount"

Copy link
Author

Choose a reason for hiding this comment

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

The problem is that the total budget depends on the block height of the superblock and that isn't well defined in general for proposals because the start/end epoch can span multiple superblocks.

So I'm not sure how to implement this.

Copy link

Choose a reason for hiding this comment

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

So I'm not sure how to implement this.

  1. Estimate blocks height at end_epoch using consensus.nPowTargetSpacing, current timestamp and current block height;
  2. Find next sb for that height, same way as in getgovernanceinfo - probably can move calculation to a standalone function e.g.
void CSuperblock::GetNearestSuperblocksHeights(int nBlockHeight, int &nLastSuperblock, int &nNextSuperblock)
{
    const Consensus::Params& consensusParams = Params().GetConsensus();
    int nSuperblockStartBlock = consensusParams.nSuperblockStartBlock;
    int nSuperblockCycle = consensusParams.nSuperblockCycle;

    // Get first superblock
    int nFirstSuperblockOffset = (nSuperblockCycle - nSuperblockStartBlock % nSuperblockCycle) % nSuperblockCycle;
    int nFirstSuperblock = nSuperblockStartBlock + nFirstSuperblockOffset;

    if(nBlockHeight < nFirstSuperblock) {
        nLastSuperblock = 0;
        nNextSuperblock = nFirstSuperblock;
    } else {
        nLastSuperblock = nBlockHeight - nBlockHeight % nSuperblockCycle;
        nNextSuperblock = nLastSuperblock + nSuperblockCycle;
    }
}

and then just reuse it in both cases;
3. Use CAmount CSuperblock::GetPaymentsLimit(int nBlockHeight) to get max total budget at nNextSuperblock.

Copy link
Author

Choose a reason for hiding this comment

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

Yes, that would work because the budget should be a monotonically decreasing function of block height so you only need to check the block height corresponding to end_epoch.

But is nPowTargetSpacing = 2.5 minutes the most accurate way to estimate block height from time ? It appears that sentinel uses 2.62 minutes. Accuracy could be quite important because theoretically the the end_epoch could be a number of months in the future.

Copy link

Choose a reason for hiding this comment

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

That's another reason why we should use block heights and estimate epochs and not the otherwise ;) OK, let's maybe write a TODO here for now.

@UdjinM6 UdjinM6 added this to the 12.2 milestone Jun 7, 2017
ParseJSONData();
}

bool CProposalValidator::Validate()
Copy link

Choose a reason for hiding this comment

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

Just checking that this is intended behaviour:
do the validations in the given order with 'early-out' returns
rather than
do all the validations with a single return concatenating any/all error messages

Copy link
Author

Choose a reason for hiding this comment

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

Yes, that's what was intended.

return false;
}

static const std::string strAllowedChars = "-_abcdefghijklmnopqrstuvwxyz012345789";
Copy link

Choose a reason for hiding this comment

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

Use const char* for this named sting literal (or put the string literal directly at the single point of use):
Avoids the need for static qualification as string literals are magically static.
Saves unnecessary std::string initialization and allocation.

Maybe put the capital letters in too, to save on the transform tolower.

Copy link
Author

Choose a reason for hiding this comment

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

I disagree, the safety advantages of standardizing on std::string far outweigh the negligible cost of the small extra memory allocation.

Copy link

@willwray willwray Jun 8, 2017

Choose a reason for hiding this comment

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

String literals, constants, are a special case and have some safety benefits
(as we are now on C++11, C-style mutable access to string literals is illegal closing that security hole).
Zero-termination is guaranteed for string literals.
They are magically static and may be held in protected memory with some compiler checks on bounds.

The suggestion to move the literal directly to its single point of use is good for safety, no?

if(strName.find_first_not_of(
   "-_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345789") != std::string::npos)

The name currently self-documents that these are allowed characters but that's clear in the code.
Apart from the documentation value there's no need for a separate variable, especially one that allocates.
The string literal is there anyway - adding more moving parts is only adding potential exploit vectors.

If there is a need for a name and const char* feels unsafe then const char[] captures the size and so can be used with iterators, for instance:

char hi[] = "hello";
std::copy(std::begin(hi),std::end(hi),std::ostream_iterator<char>(std::cout));

(free function std begin,end are C++11 and constexpr since C++14, std::size is C++17)

In this case we have a single use string literal, used in a std call that accepts char* so const char* is fine.

Allocation is another discussion, more related to my comments on string concatentation.

return false;
}

static const std::string base58chars = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
Copy link

Choose a reason for hiding this comment

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

const char* as above


std::string strURLStripped = StripWhitespace(strURL);

if(int(strURLStripped.size()) < 4) {
Copy link

Choose a reason for hiding this comment

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

Kill the cast
If the cast is there to quieten the warning of 'comparing signed and unsigned'
then use an unsigned literal instead
if(strURLStripped.size() < 4u)

fJSONValid = true;
}
catch(std::exception& e) {
strErrorMessages += std::string(e.what()) + std::string(";");
Copy link

Choose a reason for hiding this comment

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

I'd drop the second std::string and append ";" directly.
Others below.

Copy link

@willwray willwray Jun 8, 2017

Choose a reason for hiding this comment

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

More explanation on my comment.
If the error message type is changed from string to stringstream then it is more clearly redundant to convert output char* parameters to strings before streaming them out:

    strstreamErrorMessages << e.what() << ";";

Apart from type and syntax, the calling pattern is the same so there is no reason to treat the parameters differently in string vs stringstream. Sticking with string, the two string conversions on the output parameters can be dropped, with no safety repercussions:

    strErrorMessages += e.what();
    strErrorMessages +=  ';';

Choose a reason for hiding this comment

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

Compare generated code to estimate the cost in terms of code size.
The links below use Compiler Explorer https://godbolt.org,
(x86-64 gcc 6.3 with -Os option to optimise for size.)

For the baseline string concatenation with redundant string conversions:

   strErrorMessages += std::string(e.what()) + std::string(";");

Baseline >130 lines, 13 library calls

For the suggested refactor:

    strErrorMessages += e.what();
    strErrorMessages += ';';

Suggestion <10 lines 2 library calls

The cost of allocation is harder to quantify, Many small allocations add up.

{
std::istringstream istr(uValue.get_str());
istr >> dValue;
fOK = ! istr.fail();
Copy link

Choose a reason for hiding this comment

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

nitpick, inconsistent spacing.
Then again, ! does look like i and !i does my eyes in!
Feel free to ignore


std::string CProposalValidator::StripWhitespace(const std::string& strIn)
{
static const std::string strWhitespace = " \f\n\r\t\v";
Copy link

Choose a reason for hiding this comment

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

const char* as above.

*/
bool CProposalValidator::CheckURL(const std::string& strURLIn)
{
std::string strRest(strURLIn);
Copy link

Choose a reason for hiding this comment

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

As this is copying the function argument and is its only use, this std::string could be made the function argument.
'value semantics' also allows an r-value string argument to be moved in, avoiding the copy allocation.


// Process netloc
if((strRest.size() > 2) && (strRest.substr(0,2) == "//")) {
static const std::string strNetlocDelimiters = "/?#";
Copy link

Choose a reason for hiding this comment

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

const char*

}

if(dValue <= 0.0) {
strErrorMessages += "payment_amount invalid;";
Copy link

Choose a reason for hiding this comment

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

Also Validate() concatenates "Invalid payment amount"

Copy link

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

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

Since this is not a performance or consensus critical part and this code is going to be rarely used at all I don't see a need to micro- or over-optimization here.

utACK

@willwray
Copy link

utACK

Updating string handling patterns in dash or bitcoin code calls for wider discussion.

@UdjinM6 UdjinM6 merged commit a109a61 into dashpay:v0.12.2.x Jun 26, 2017
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.

3 participants