What the ledger does not check

An XRPL escrow with a condition looks like a guarantee. It is a guarantee of something narrower than most people assume, and the gap is where a lender loses money.

Every Straid loan is backed by XRP sitting in an escrow on the XRP Ledger. The borrower's own wallet creates that escrow, and for the life of the loan the ledger holds it. We can only move it in the one way the escrow allows: by finishing it, which sends the XRP to our collateral account, from where a repaid borrower is paid straight back.

That is the whole product in two sentences, and it is also where most people stop asking questions. It is worth asking one more, because a conditional escrow guarantees something narrower than it appears to.

How the escrow works

An EscrowCreate carries an amount, a destination, a cancel time and, in our case, a condition. The condition is a PREIMAGE-SHA-256 crypto-condition: 39 bytes, made of the prefix A0258020, the SHA-256 of a 32 byte secret, and the suffix 810120 which states that the preimage is 32 bytes long. Finishing the escrow early means submitting an EscrowFinish with the 36 byte fulfilment: A0228020 followed by the secret itself.

Two parties can end a Straid escrow. We can, because the secret for each loan is derived inside a key service and never leaves it in the clear. And once the escrow's CancelAfter time has passed, anybody at all can cancel it, which returns the XRP to the borrower. The second one matters more than the first. It is the borrower's way out if we stop existing, and it needs nothing from us.

What rippled checks, and what it does not

When an escrow is created, the ledger checks that the condition is well formed. It does not check that anybody is able to fulfil it, and it has no way to: a hash is a hash, and the ledger has never seen the secret behind it.

Picture an escrow that would pass a quick review. Right destination, right amount, right cancel time, a valid condition, funded and validated. Now suppose the secret behind that condition was generated by the borrower on their own laptop. From the outside it is identical to one of ours. A lender who checked only that a funded escrow exists, and then paid out, would have made an unsecured loan with extra steps and a false sense of safety.

The same applies to smaller substitutions. A different destination tag, a cancel time a year out, a ticketed create, an amount ten drops short. Each one leaves a loan that cannot be closed the way the agreement says it will be.

So we compare, byte for byte

We prepare the entire transaction. The wallet may add only what a wallet has to add: Fee, Sequence, LastLedgerSequence, SigningPubKey and TxnSignature, plus a Flags value that is zero or the canonical-signature flag, and a SourceTag which is recorded and never matched. Anything else is a rejection. After the escrow appears in a validated ledger, three things are checked before a single RLUSD moves.

  • The signed blob. It is decoded and compared field by field against what we prepared, with an allowlist of what the wallet may add and a reject list for the rest. This step is the one that catches fields such as TicketSequence, Delegate, AccountTxnID and Memos, because those leave no trace at all in the escrow entry that ends up in the ledger.
  • The creation metadata. The escrow's ledger index is recomputed from the account and the sequence, then matched against the CreatedNode the transaction actually produced. This is an independent check, and it is what catches a ticketed create.
  • The escrow entry. The fields it carries are compared again, and FinishAfter has to be absent, since a finish time would let the escrow be closed by the clock instead of by the secret.

Every defect is collected rather than stopping at the first one, so a rejected escrow produces a list and not a guess. If anything fails, nothing is paid out and the borrower's XRP returns to them at the cancel time. They are out the ledger fee, which is a fraction of a cent, and nothing else.

What this does not do

It is a defence against one specific thing: an escrow that is not the escrow we agreed to. It does nothing about the price of XRP falling, about our own operational mistakes, or about anything that happens to RLUSD, which Ripple issues and we merely lend. A borrower who signs the right escrow and then watches XRP halve still has a loan that can be called.

It also does not take us out of the picture, and we would rather say so than dress it up. For the life of the loan the ledger holds the collateral. On a default we take it through the escrow. On repayment with the release option, it passes through our account for exactly one transaction on the way back to the borrower. Those are real powers, held by people you can write to.

If you want to see those fields for yourself, the contract they come from is public: the API is documented and the prepared transaction is what POST /v1/loan-intents returns, next to the exact field set the verifier will compare it against.

Back to the notes