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

Making well-known pre-approved transactions useful #51

Closed
roosmaa opened this issue Oct 7, 2017 · 6 comments
Closed

Making well-known pre-approved transactions useful #51

roosmaa opened this issue Oct 7, 2017 · 6 comments
Labels
help wanted Open especially for those who want to write a CAP/SEP! needs draft This an issue that has no corresponding draft, and as such has not entered the CAP/SEP process.

Comments

@roosmaa
Copy link

roosmaa commented Oct 7, 2017

The ability to authorize certain transactions by approving their hash under the account signers is quite a powerful concept. It also has the potential to cater for a certain subset of smart contracts, if used creatively. A state machine can be constructed with the prepared transactions that make up a chain of valid actions that can be executed.

For example, a near-zero-trust escrow account contract could be constructed using those principles. In an escrow smart contract we need 3 parties: the sender, the recipient and the 3rd party dispute resolver. The contract needs to support the following state flows:

  1. consumer [sender] deposits funds -> consumer [sender] releases funds once the goods have been received
  2. consumer [sender] deposits funds -> merchant [recipient] returns funds to consumer as they were unable to hold up their end of the bargain
  3. consumer [sender] deposits funds -> merchant [recipient] starts a dispute because the goods were delivered but the consumer didn't release funds -> dispute resolver examines the facts and determines which party gets the escrowed funds
  4. consumer [sender] deposits funds -> merchant [recipient] has not responded in 30 days, the escrow account will be unlocked and funds returned to consumer [sender]

This can be achieved by preparing the following transactions:

  1. completeEscrow: sender releases funds to the recipient
  2. refundEscrow: recipient releases funds back to sender
  3. beginDispute: recipient adds 2 dispute resolution transaction hashes (completeDisputedEscrow, refund_disputed_escrow) to the account
  4. unlockExpiredEscrow: sender unlocks the account to transfer funds back to themselves (time locked)
  5. completeDisputedEscrow: dispute resolver releases funds to recipient subtracting dispute handling fee from the funds
  6. refundDisputedEscrow: dispute resolver releases funds back to sender subtracting dispute handling fee from the funds

In order for all parties to trust the contract, the transactions which produce the pre-authorized hashes need to be known by all parties. In effect those transactions can be considered well-known and public knowledge.

In order to setup the transactions in that way we need to rely on couple of assumptions:

  • Transactions follow ACID
    • Transactions are constructed to expect certain amount of assets to be available on the escrow account and will fail if there aren't, in a waythat they could be repeated later when there are enough funds
    • Transactions need the ability to limit who can execute them and in case the wrong signer tries to execute them they need to fail, only to be later succeeded when signed by the correct signer
  • Account sequence number is used to lock out a set of transactions once one of them has been executed

My first attempt at limiting transactions to certain signers was to add the signer and increasing the required weights at the start of the transaction and remove them at the end. But apparently the transaction signature validation happens too early for this kind of approach to work (stellar/stellar-core#1329). My second attempt at this was to add the signers to the account and remove the disallowed signers at the beginning of the transaction which results in the transaction failing for wrong users, which by transaction ACID promises made by documentation should work fine. See below for example code of constructing the transactions.

But in reality even the failed transaction manages to increase the account sequence number according to stellar/stellar-core#1330 . Which basically makes well-known public transactions useless as anyone can try to execute them and make them invalid for the future. For example, in our previous escrow example the sender could take the transaction meant for the recipient, submit it with the wrong signatures and the recipient wouldn't ever be able to use that transaction themselves.

In stellar/stellar-core#1330 vogel suggested proposing changes to the protocol that would allow transactions with flexible sequence number to be preauthorized. However, I personally feel that the whole sequence number incremented when the actual transaction failed to apply makes more sense to "fix".

Though, of course, I'm not that versed in the nitty-gritty of the protocol to know why this kind of design decision was made in the first place. Maybe it's for a good reason (protecting against spammy behaviour or some such). Though in that case claiming that the transaction are ACID is somewhat false and the actual usecases for the pre-authorized transaction hashes drops significantly.

I would like to start a discussion around how to make such contracts possible on the Stellar network. And if such usecase is even something that the core team considers worthwhile?

JS code example of constructing an escrow account contract

function prepareEscrowTransactions(escrowKeypair, escrowSequence, escrowExpires, senderSignKeypair, recipientSignKeypair) {
  var escrowSequence = new BigNumber(escrowSequence);

  var completeEscrowTx = new StellarSdk.TransactionBuilder(new StellarSdk.Account(escrowKeypair.publicKey(), escrowSequence.add(1).toString()), {
      timebounds: {
        minTime: 0,
        maxTime: escrowExpires,
      },
    })
    // Remove keys that aren't allowed to perform this transaction
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        ed25519PublicKey: recipientSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    // Complete escrow payment
    .addOperation(StellarSdk.Operation.payment({
      destination: recipientKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: escrowAmount.toString(),
    }))
    .addOperation(StellarSdk.Operation.payment({
      destination: providerKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: providerFeeAmount.toString(),
    }))
    // Hand over account access back to the provider
    .addOperation(StellarSdk.Operation.setOptions({
      masterWeight: 1,
      lowThreshold: 1,
      medThreshold: 1,
      highThreshold: 1,
      signer: {
        ed25519PublicKey: senderSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    .build();

  var refundEscrowTx = new StellarSdk.TransactionBuilder(new StellarSdk.Account(escrowKeypair.publicKey(), escrowSequence.add(1).toString()), {
      timebounds: {
        minTime: 0,
        maxTime: escrowExpires,
      }
    })
    // Remove keys that aren't allowed to perform this transaction
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        ed25519PublicKey: senderSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    // Cancel the escrow payment
    .addOperation(StellarSdk.Operation.payment({
      destination: senderKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: escrowAmount.toString(),
    }))
    .addOperation(StellarSdk.Operation.payment({
      destination: providerKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: providerFeeAmount.toString(),
    }))
    // Hand over account access back to the provider
    .addOperation(StellarSdk.Operation.setOptions({
      masterWeight: 1,
      lowThreshold: 1,
      medThreshold: 1,
      highThreshold: 1,
      signer: {
        ed25519PublicKey: recipientSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    .build();

  var completeDisputedEscrowTx = new StellarSdk.TransactionBuilder(new StellarSdk.Account(escrowKeypair.publicKey(), escrowSequence.add(2).toString()))
    .addOperation(StellarSdk.Operation.payment({
      destination: recipientKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: (escrowAmount - disputeFeeAmount).toString(),
    }))
    .addOperation(StellarSdk.Operation.payment({
      destination: providerKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: (providerFeeAmount + disputeFeeAmount).toString(),
    }))
    // Hand over account access back to the provider
    .addOperation(StellarSdk.Operation.setOptions({
      masterWeight: 1,
      lowThreshold: 1,
      medThreshold: 1,
      highThreshold: 1,
    }))
    .build();

  var refundDisputedEscrowTx = new StellarSdk.TransactionBuilder(new StellarSdk.Account(escrowKeypair.publicKey(), escrowSequence.add(2).toString()))
    .addOperation(StellarSdk.Operation.payment({
      destination: senderKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: (escrowAmount - disputeFeeAmount).toString(),
    }))
    .addOperation(StellarSdk.Operation.payment({
      destination: providerKeypair.publicKey(),
      asset: StellarSdk.Asset.native(),
      amount: (providerFeeAmount + disputeFeeAmount).toString(),
    }))
    // Hand over account access back to the provider
    .addOperation(StellarSdk.Operation.setOptions({
      masterWeight: 1,
      lowThreshold: 1,
      medThreshold: 1,
      highThreshold: 1,
    }))
    .build();

  var beginDisputeTx = new StellarSdk.TransactionBuilder(new StellarSdk.Account(escrowKeypair.publicKey(), escrowSequence.add(1).toString()), {
      timebounds: {
        minTime: 0,
        maxTime: escrowExpires,
      },
    })
    // Remove keys that aren't allowed to perform this transaction
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        ed25519PublicKey: senderSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    // Prepare the dispute transactions
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        preAuthTx: completeDisputedEscrowTx.hash(),
        weight: 2,
      },
    }))
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        preAuthTx: refundDisputedEscrowTx.hash(),
        weight: 2,
      },
    }))
    .addOperation(StellarSdk.Operation.setOptions({
      masterWeight: 1,
      lowThreshold: 3,
      medThreshold: 3,
      highThreshold: 3,
      signer: {
        ed25519PublicKey: recipientSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    .build();

  var unlockExpiredEscrowTx = new StellarSdk.TransactionBuilder(new StellarSdk.Account(escrowKeypair.publicKey(), escrowSequence.add(1).toString()), {
      timebounds: {
        minTime: escrowExpires,
        maxTime: 0,
      }
    })
    // Hand over account access back to the provider
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        ed25519PublicKey: senderSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    .addOperation(StellarSdk.Operation.setOptions({
      signer: {
        ed25519PublicKey: recipientSignKeypair.publicKey(),
        weight: 0,
      },
    }))
    .addOperation(StellarSdk.Operation.setOptions({
      masterWeight: 1,
      lowThreshold: 1,
      medThreshold: 1,
      highThreshold: 1,
    }))
    .build();

  return {
    completeEscrow: completeEscrowTx,
    refundEscrow: refundEscrowTx,
    beginDispute: beginDisputeTx,
    completeDisputedEscrow: completeDisputedEscrowTx,
    refundDisputedEscrow: refundDisputedEscrowTx,
    unlockExpiredEscrow: unlockExpiredEscrowTx,
  }
}

var txs = prepareEscrowTransactions(escrowKeypair, escrowSequence, escrowExpires, senderSignKeypair, recipientSignKeypair);

var tx = new StellarSdk.TransactionBuilder(escrowAccount)
  .addOperation(StellarSdk.Operation.setOptions({
    signer: {
      ed25519PublicKey: senderSignKeypair.publicKey(),
      weight: 1,
    },
  }))
  .addOperation(StellarSdk.Operation.setOptions({
    signer: {
      ed25519PublicKey: recipientSignKeypair.publicKey(),
      weight: 1,
    },
  }))
  .addOperation(StellarSdk.Operation.setOptions({
    signer: {
      preAuthTx: txs.completeEscrow.hash(),
      weight: 2,
    },
  }))
  .addOperation(StellarSdk.Operation.setOptions({
    signer: {
      preAuthTx: txs.refundEscrow.hash(),
      weight: 2,
    },
  }))
  .addOperation(StellarSdk.Operation.setOptions({
    signer: {
      preAuthTx: txs.beginDispute.hash(),
      weight: 2,
    },
  }))
  .addOperation(StellarSdk.Operation.setOptions({
    signer: {
      preAuthTx: txs.unlockExpiredEscrow.hash(),
      weight: 2,
    },
  }))
  .addOperation(StellarSdk.Operation.setOptions({
    masterWeight: 0,
    lowThreshold: 3,
    medThreshold: 3,
    highThreshold: 3,
  }))
  .build();

tx.sign(escrowKeypair);
server.submitTransaction(tx);
@MonsieurNicolas
Copy link
Contributor

Yes we're working on making this better.

In the meanwhile, I think that for this particular case you can achieve what you want by taking advantage of an escrow account where you set its master weight to 0.

The property that you want to use is that updating the signers should be done with the last operations within your transaction (to enable/disable the next edges in the graph), not as a way to modify the current transaction; you also need to make sure that the order of those operations are always valid from a signature point of view (see example below for transaction ID #2).
Basically at the end of each "state transition", you want to update the signers (edges) of the graph that make valid transitions.

Also, something to note is that if a transaction is submitted to the network with a sequence number that doesn't match the current account's counter, it will be rejected without consuming the sequence number.

I used sequence number 1 as the sequence number for the escrow account, but of course you'd need to adjust it to be whatever the escrow account has.

I also added an extra step, I think you were missing a transition that lets the consumer acknowledge receipt of the goods.

Also, I use Cleanup here as shorthand for the following operations: { delete trustline(s), merge account -> consumer } or { restore master weight to 10 }

Transactions should only be signed before submitting to the network in this particular case.

Tx #1 Signature: (setup by Consumer)

seq=1 Escrow account setup with escrow amount.

   Update signers to:
        1. h(#4) @ 10
        2. h(#2) @ 8
        3. Merchant @ 2
        4. Consumer @ 1
        5. Resolver @ 1

   Update weights to:
        1. Master = 0
        2. Low=Med=High=10

Tx #2 Signature: Merchant

seq=2 Updated - Merchant said they shipped (no-op= send 1 XLM to self).

Update signers to:
        1. Delete h(#4) (deletes not strictly required but help with cleanup)
        2. Set h(#5) @ 8
        3. Set h(#6) @ 8
        4. Set h(#7) @ 8
        5. Set Consumer @2
        6. Set Resolver @ 1
        7. Set Merchant @10 <<< this is important as the remaining weights would not work out
        8. Delete h(#2)
        9. Set Merchant @1

Tx #3 Signature: Merchant

seq=2 Merchant refunds consumer & account cleanup

Tx #4 Signature: None

seq=2 Valid after T+30days: "UnlockExpiredEscrow" refund & cleanup

Tx #5 Signature: Consumer

seq=3 New: Consumer acks receipt, release amount from escrow, cleanup

Tx #6 Signature: Merchant

seq=3 Valid after T+5 days: Merchant - starts dispute "consumer didn't ack/release funds"

Update signers to:
        1. Delete h(#5)
        2. Delete h(#6)
        3. Delete h(#7)
        4. h(#8) @ 8
        5. h(#9) @ 8
        6. Merchant @ 1
        7. Consumer @ 1
        8. Resolver @ 2

Tx #7 Signature: Consumer

seq=3 Updated - Consumer starts dispute: "not received"/"not what I wanted" (follows #2)

Update signers to:
        1. Delete h(#5)
        2. Delete h(#6)
        3. Delete h(#7)
        4. h(#8) @ 8
        5. h(#9) @ 8
        6. Merchant @ 1
        7. Consumer @ 1
        8. Resolver @ 2

Tx #8 Signature: Resolver, Merchant

seq=4 completeDisputedEscrow: send funds to merchant, fee to Resolver & cleanup

Tx #9 Signature: Resolver, Consumer

seq=4 refundDisputedEscrow: send funds back to consumer, fee to resolver & cleanup

@roosmaa
Copy link
Author

roosmaa commented Oct 11, 2017

In Tx#2:

  1. Set Merchant @10 <<< this is important as the remaining weights would not work out
  2. Delete h(Currency codes should allow more than 3 characters #2)
  3. Set Merchant @1

Including the tx hash of itself is kind of difficult. I recall the documentation saying that the hash of the signed contract would be removed automatically.

Tho it does bring up another idea - maybe in the future, the transaction seq number could be included besides the hash, so once it becomes impossible to execute that tx it would be automatically removed? It would also help with long chain of transactions that are dependent on oneanother which makes removing their hashes difficult and freeying up the 20 available signer slots is sometimes needed for large graph.

@MonsieurNicolas
Copy link
Contributor

Ah yes, sorry the delete h(#2) in Tx #2 is implicit and not required like you said; you should still be able to implement the workflow.

I agree, I think we need to make those one time signatures easier to manage. I don't know what should be enabled just yet at the protocol layer though (a condition on sequence number is just one of the conditions that people may want).

One of the steps here that is not easy to do is "cleanup" that cannot be a mergeaccount operation if the tx was authorized by a one time hash so the only way right now it seems is to restore the master weight to something sane and let the person with the secret key do the cleanup.

@MonsieurNicolas
Copy link
Contributor

btw, I opened #53 that should help reduce the need to rely on h(transaction) (and instead just use sequence numbers when possible).

@theaeolianmachine theaeolianmachine added needs draft This an issue that has no corresponding draft, and as such has not entered the CAP/SEP process. help wanted Open especially for those who want to write a CAP/SEP! and removed incomplete labels Mar 11, 2019
@theaeolianmachine
Copy link
Contributor

@roosmaa, do you feel a core enhancement (CAP) or ecosystem proposal (SEP) is needed, or can we close this out?

@roosmaa
Copy link
Author

roosmaa commented Mar 12, 2019

@theaeolianmachine it's been so long since I created this issue that I cannot really answer that question. So let's close this.

@roosmaa roosmaa closed this as completed Mar 12, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Open especially for those who want to write a CAP/SEP! needs draft This an issue that has no corresponding draft, and as such has not entered the CAP/SEP process.
Projects
None yet
Development

No branches or pull requests

5 participants
@roosmaa @jedmccaleb @theaeolianmachine @MonsieurNicolas and others