Clarify Escrow semantics (RIPD-1571):

When creating an escrow, if the `CancelAfter` time is specified but
the `FinishAfter` is not, the resulting escrow can be immediately
completed using `EscrowFinish`. While this behavior is documented,
it is unintuitive and can be confusing for users.

This commit introduces a new fix amendment (fix1571) which prevents
the creation of new Escrow entries that can be finished immediately
and without any requirements.

Once the amendment is activated, creating a new Escrow will require
specifying the `FinishAfter` time explicitly or requires that a
cryptocondition be specified.
This commit is contained in:
Nikolaos D. Bougalis
2018-03-02 06:23:17 -08:00
parent 2b8893dfca
commit 8d9dffcf84
4 changed files with 685 additions and 440 deletions

View File

@@ -41,99 +41,52 @@
namespace ripple { namespace ripple {
/* /*
Escrow allows an account holder to sequester any amount Escrow
of XRP in its own ledger entry, until the escrow process ======
either finishes or is canceled.
If the escrow process finishes successfully, then the Escrow is a feature of the XRP Ledger that allows you to send conditional
destination account (which must exist) will receives the XRP payments. These conditional payments, called escrows, set aside XRP and
sequestered XRP. If the escrow is, instead, canceled, deliver it later when certain conditions are met. Conditions to successfully
the account which created the escrow will receive the finish an escrow include time-based unlocks and crypto-conditions. Escrows
sequestered XRP back instead. can also be set to expire if not finished in time.
EscrowCreate The XRP set aside in an escrow is locked up. No one can use or destroy the
XRP until the escrow has been successfully finished or canceled. Before the
expiration time, only the intended receiver can get the XRP. After the
expiration time, the XRP can only be returned to the sender.
When an escrow is created, an optional condition may For more details on escrow, including examples, diagrams and more please
be attached. If present, that condition must be visit https://ripple.com/build/escrow/#escrow
fulfilled for the escrow to successfully finish.
At the time of creation, one or both of the fields For details on specific transactions, including fields and validation rules
sfCancelAfter and sfFinishAfter may be provided. If please see:
neither field is specified, the transaction is
malformed.
Since the escrow eventually becomes a payment, an `EscrowCreate`
optional DestinationTag and an optional SourceTag --------------
are supported in the EscrowCreate transaction. See: https://ripple.com/build/transactions/#escrowcreate
Validation rules: `EscrowFinish`
--------------
See: https://ripple.com/build/transactions/#escrowfinish
sfCondition `EscrowCancel`
If present, specifies a condition; the same --------------
condition along with its matching fulfillment See: https://ripple.com/build/transactions/#escrowcancel
are required during EscrowFinish.
sfCancelAfter
If present, escrow may be canceled after the
specified time (seconds after the Ripple epoch).
sfFinishAfter
If present, must be prior to sfCancelAfter.
A EscrowFinish succeeds only in ledgers after
sfFinishAfter but before sfCancelAfter.
If absent, same as parentCloseTime
Malformed if both sfCancelAfter, sfFinishAfter
are absent.
Malformed if both sfFinishAfter, sfCancelAfter
specified and sfCancelAfter <= sfFinishAfter
EscrowFinish
Any account may submit a EscrowFinish. If the escrow
ledger entry specifies a condition, the EscrowFinish
must provide the same condition and its associated
fulfillment in the sfCondition and sfFulfillment
fields, or else the EscrowFinish will fail.
If the escrow ledger entry specifies sfFinishAfter, the
transaction will fail if parentCloseTime <= sfFinishAfter.
EscrowFinish transactions must be submitted before
the escrow's sfCancelAfter if present.
If the escrow ledger entry specifies sfCancelAfter, the
transaction will fail if sfCancelAfter <= parentCloseTime.
NOTE: The reason the condition must be specified again
is because it must always be possible to verify
the condition without retrieving the escrow
ledger entry.
EscrowCancel
Any account may submit a EscrowCancel transaction.
If the escrow ledger entry does not specify a
sfCancelAfter, the cancel transaction will fail.
If parentCloseTime <= sfCancelAfter, the transaction
will fail.
When a escrow is canceled, the funds are returned to
the source account.
By careful selection of fields in each transaction,
these operations may be achieved:
* Lock up XRP for a time period
* Execute a payment conditionally
*/ */
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
/** Has the specified time passed?
@param now the current time
@param mark the cutoff point
@return true if \a now refers to a time strictly after \a mark, false otherwise.
*/
static inline bool after (NetClock::time_point now, std::uint32_t mark)
{
return now.time_since_epoch().count() > mark;
}
XRPAmount XRPAmount
EscrowCreate::calculateMaxSpend(STTx const& tx) EscrowCreate::calculateMaxSpend(STTx const& tx)
{ {
@@ -156,14 +109,26 @@ EscrowCreate::preflight (PreflightContext const& ctx)
if (ctx.tx[sfAmount] <= beast::zero) if (ctx.tx[sfAmount] <= beast::zero)
return temBAD_AMOUNT; return temBAD_AMOUNT;
if (! ctx.tx[~sfCancelAfter] && // We must specify at least one timeout value
! ctx.tx[~sfFinishAfter]) if (! ctx.tx[~sfCancelAfter] && ! ctx.tx[~sfFinishAfter])
return temBAD_EXPIRATION; return temBAD_EXPIRATION;
// If both finish and cancel times are specified then the cancel time must
// be strictly after the finish time.
if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] && if (ctx.tx[~sfCancelAfter] && ctx.tx[~sfFinishAfter] &&
ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter]) ctx.tx[sfCancelAfter] <= ctx.tx[sfFinishAfter])
return temBAD_EXPIRATION; return temBAD_EXPIRATION;
if (ctx.rules.enabled(fix1571))
{
// In the absence of a FinishAfter, the escrow can be finished
// immediately, which can be confusing. When creating an escrow,
// we want to ensure that either a FinishAfter time is explicitly
// specified or a completion condition is attached.
if (! ctx.tx[~sfFinishAfter] && ! ctx.tx[~sfCondition])
return temMALFORMED;
}
if (auto const cb = ctx.tx[~sfCondition]) if (auto const cb = ctx.tx[~sfCondition])
{ {
using namespace ripple::cryptoconditions; using namespace ripple::cryptoconditions;
@@ -193,20 +158,36 @@ EscrowCreate::doApply()
{ {
auto const closeTime = ctx_.view ().info ().parentCloseTime; auto const closeTime = ctx_.view ().info ().parentCloseTime;
if (ctx_.tx[~sfCancelAfter]) // Prior to fix1571, the cancel and finish times could be greater
// than or equal to the parent ledgers' close time.
//
// With fix1571, we require that they both be strictly greater
// than the parent ledgers' close time.
if (ctx_.view ().rules().enabled(fix1571))
{ {
auto const cancelAfter = ctx_.tx[sfCancelAfter]; if (ctx_.tx[~sfCancelAfter] && after(closeTime, ctx_.tx[sfCancelAfter]))
return tecNO_PERMISSION;
if (closeTime.time_since_epoch().count() >= cancelAfter) if (ctx_.tx[~sfFinishAfter] && after(closeTime, ctx_.tx[sfFinishAfter]))
return tecNO_PERMISSION; return tecNO_PERMISSION;
} }
else
if (ctx_.tx[~sfFinishAfter])
{ {
auto const finishAfter = ctx_.tx[sfFinishAfter]; if (ctx_.tx[~sfCancelAfter])
{
auto const cancelAfter = ctx_.tx[sfCancelAfter];
if (closeTime.time_since_epoch().count() >= finishAfter) if (closeTime.time_since_epoch().count() >= cancelAfter)
return tecNO_PERMISSION; return tecNO_PERMISSION;
}
if (ctx_.tx[~sfFinishAfter])
{
auto const finishAfter = ctx_.tx[sfFinishAfter];
if (closeTime.time_since_epoch().count() >= finishAfter)
return tecNO_PERMISSION;
}
} }
auto const account = ctx_.tx[sfAccount]; auto const account = ctx_.tx[sfAccount];
@@ -383,17 +364,35 @@ EscrowFinish::doApply()
if (! slep) if (! slep)
return tecNO_TARGET; return tecNO_TARGET;
// Too soon? // If a cancel time is present, a finish operation should only succeed prior
if ((*slep)[~sfFinishAfter] && // to that time. fix1571 corrects a logic error in the check that would make
ctx_.view().info().parentCloseTime.time_since_epoch().count() <= // a finish only succeed strictly after the cancel time.
(*slep)[sfFinishAfter]) if (ctx_.view ().rules().enabled(fix1571))
return tecNO_PERMISSION; {
auto const now = ctx_.view().info().parentCloseTime;
// Too late? // Too soon: can't execute before the finish time
if ((*slep)[~sfCancelAfter] && if ((*slep)[~sfFinishAfter] && ! after(now, (*slep)[sfFinishAfter]))
(*slep)[sfCancelAfter] <= return tecNO_PERMISSION;
ctx_.view().info().parentCloseTime.time_since_epoch().count())
return tecNO_PERMISSION; // Too late: can't execute after the cancel time
if ((*slep)[~sfCancelAfter] && after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
}
else
{
// Too soon?
if ((*slep)[~sfFinishAfter] &&
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
(*slep)[sfFinishAfter])
return tecNO_PERMISSION;
// Too late?
if ((*slep)[~sfCancelAfter] &&
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
(*slep)[sfCancelAfter])
return tecNO_PERMISSION;
}
// Check cryptocondition fulfillment // Check cryptocondition fulfillment
{ {
@@ -515,17 +514,32 @@ EscrowCancel::preflight (PreflightContext const& ctx)
TER TER
EscrowCancel::doApply() EscrowCancel::doApply()
{ {
auto const k = keylet::escrow( auto const k = keylet::escrow(ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
ctx_.tx[sfOwner], ctx_.tx[sfOfferSequence]);
auto const slep = ctx_.view().peek(k); auto const slep = ctx_.view().peek(k);
if (! slep) if (! slep)
return tecNO_TARGET; return tecNO_TARGET;
// Too soon? if (ctx_.view ().rules().enabled(fix1571))
if (! (*slep)[~sfCancelAfter] || {
ctx_.view().info().parentCloseTime.time_since_epoch().count() <= auto const now = ctx_.view().info().parentCloseTime;
// No cancel time specified: can't execute at all.
if (! (*slep)[~sfCancelAfter])
return tecNO_PERMISSION;
// Too soon: can't execute before the cancel time.
if (! after(now, (*slep)[sfCancelAfter]))
return tecNO_PERMISSION;
}
else
{
// Too soon?
if (!(*slep)[~sfCancelAfter] ||
ctx_.view().info().parentCloseTime.time_since_epoch().count() <=
(*slep)[sfCancelAfter]) (*slep)[sfCancelAfter])
return tecNO_PERMISSION; return tecNO_PERMISSION;
}
AccountID const account = (*slep)[sfAccount]; AccountID const account = (*slep)[sfAccount];
// Remove escrow from owner directory // Remove escrow from owner directory

View File

@@ -75,7 +75,8 @@ class FeatureCollections
"fix1523", "fix1523",
"fix1528", "fix1528",
"DepositAuth", "DepositAuth",
"Checks" "Checks",
"fix1571"
}; };
std::vector<uint256> features; std::vector<uint256> features;
@@ -357,6 +358,7 @@ extern uint256 const fix1523;
extern uint256 const fix1528; extern uint256 const fix1528;
extern uint256 const featureDepositAuth; extern uint256 const featureDepositAuth;
extern uint256 const featureChecks; extern uint256 const featureChecks;
extern uint256 const fix1571;
} // ripple } // ripple

View File

@@ -108,7 +108,8 @@ detail::supportedAmendments ()
{ "B9E739B8296B4A1BB29BE990B17D66E21B62A300A909F25AC55C22D6C72E1F9D fix1523" }, { "B9E739B8296B4A1BB29BE990B17D66E21B62A300A909F25AC55C22D6C72E1F9D fix1523" },
{ "1D3463A5891F9E589C5AE839FFAC4A917CE96197098A1EF22304E1BC5B98A454 fix1528" }, { "1D3463A5891F9E589C5AE839FFAC4A917CE96197098A1EF22304E1BC5B98A454 fix1528" },
{ "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"}, { "F64E1EABBE79D55B3BB82020516CEC2C582A98A6BFE20FBE9BB6A0D233418064 DepositAuth"},
{ "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"} { "157D2D480E006395B76F948E3E07A45A05FE10230D88A7993C71F97AE4B1F2D1 Checks"},
{ "7117E2EC2DBF119CA55181D69819F1999ECEE1A0225A7FD2B9ED47940968479C fix1571" }
}; };
return supported; return supported;
} }
@@ -158,5 +159,6 @@ uint256 const fix1523 = *getRegisteredFeature("fix1523");
uint256 const fix1528 = *getRegisteredFeature("fix1528"); uint256 const fix1528 = *getRegisteredFeature("fix1528");
uint256 const featureDepositAuth = *getRegisteredFeature("DepositAuth"); uint256 const featureDepositAuth = *getRegisteredFeature("DepositAuth");
uint256 const featureChecks = *getRegisteredFeature("Checks"); uint256 const featureChecks = *getRegisteredFeature("Checks");
uint256 const fix1571 = *getRegisteredFeature("fix1571");
} // ripple } // ripple

File diff suppressed because it is too large Load Diff