Core transaction type restructuring

(Andrius Dagys) #1

This is a proposal for simplifying the transaction building and resolution logic.

Here is a rough example of the flow we have right now to build a transaction and get it signed by a counterparty:

The sender:

val builder = TransactionBuilder(General, specificNotary)

val signedTx: SignedTransaction = serviceHub.signInitialTransaction(regTxBuilder)

val counterPartySignature = sendAndReceive<DigitalSignature.WithKey>(counterparty, signedTx).unwrap { it }

val finalTx = signedTx + counterPartySignature

// Problem 1. Transaction builder essentially outputs a WireTransaction, dropping information such as input states,
// which we need to load again in finality flow to be able to verify the transaction – that seems unnecessary

The receiver:

val signedTx: SignedTransaction = receive<SignedTransaction>(counterparty).unwrap { it }

// Check signatures
signedTx.verifySignatures(allowedToBeMissing = me, notaryKey)

// Resolve dependencies
subFlow(ResolveTransactionsFlow(signedTx, counterparty))
// Resolve transaction
val wireTx = signedTx.tx
val ledgerTx = wireTx.toLedgerTransaction(serviceHub)
// Run contracts
// Problem: 2 the flow writer has to be aware of 3 transaction types and their lifecycle: 
// SignedTransaction -> WireTransaction -> LedgerTransaction, 
// along with what what verification should be performed on each type. 

// Do custom checks
ledgerTx.inputs.single() is MyContractState

// Add my signature
val mySignature = serviceHub.createSignature(signedTx)


The proposal is to modify the transaction types so only a fully resolved LedgerTransaction is ever exposed in the flows:

  1. LedgerTransaction is modified to hold (and gather) signatures and perform both contract and signature verification. Signatures get dropped when sending it to a contract.
  2. TransactionBuilder now outputs a LedgerTransaction. When sending it to a counterparty, a WireTransaction gets extracted and sent along with the signature(s).
  3. On the receiving end, a SignedTransaction holds a WireTransaction and signatures (unchanged). We automatically resolve dependencies, resolve the LedgerTransaction and run contract verification (we can probably only resolve one step back and verify the received transaction so we can avoid full dependency resolution for a gibberish transaction).
  4. The flow now receives a validated LedgerTransaction with signatures. It can perform signature verification, also expecting some signatures to be missing.
  5. The id of a LedgerTransaction can be calculated by extracting a WireTransaction and calculating the merkle root lazily.

Rough implementation plan:

  1. Add signatures field to LedgerTransaction, deprecate and copy over all signature checking methods from SignedTransaction. Allow LedgerTransaction to have an empty list of signatures for the interim.
  2. Modify TransactionBuilder to retain all data that was passed in. Introduce a build() method that returns a LedgerTransaction (TBD). Deprecate but modify toWireTransaction to work for now.
  3. Add helper methods to FlowLogic, e.g. receiveAndVerifyTransaction(): LedgerTransaction that handles dependency resolution and contract verification.
  4. Update all docs and examples to use the new API.
  5. Incrementally remove the usages of deprecated API, fix unit tests
  6. Remove deprecated API.

Here’s how the same flow would look with the new API:

The sender:

val ledgerTransaction: LedgerTransaction = TransactionBuilder(General, specificNotary)

val signedTx: LedgerTransaction = serviceHub.signTransaction(ledgerTransaction)

val counterPartySignature = sendAndReceive<DigitalSignature.WithKey>(counterparty, signedTx).unwrap { it }

val finalTx: LedgerTransaction = signedTx + counterPartySignature

// The sender doesn't need to perform any dependency resolution
// When recording the transaction, a SignedTransaction(WireTransaction, signatures) is extracted and
// stored, but it's all done inside service hub. Loading a transaction from storage also automatically resolves it
// to a LedgerTransaction

The receiver:

val transaction: LedgerTransaction = receiveAndVerifyTransaction(counterparty)

// At this point we're guaranteed that the transaction is valid, only need to check signatures
transaction.verifySignatures(allowedToBeMissing = me, notaryKey)

// Do custom checks
transaction.inputs.single() is MyContractState

// Add my signature
val mySignature = serviceHub.createSignature(transaction)


It definitely requires a bit more thought in some places, but that’s the general idea. Any thoughts or comments much appreciated!

(Andrius Dagys) #2

As a consequence of this change we could also get rid of the TransactionState wrapper, by moving the encumbrance and notary pointers out of the states. Since no notary change is allowed in a regular transaction, we can just use the transaction’s notary field as an indicator for all the output states’ notaries. That would eliminate the “check no notary change” verification step.

(Mike Hearn) #3

It looks reasonable - can you talk to Roger and Matthew about this and ask them to comment? They have also looked at simplifications in this area and came up with a different proposal, but I don’t recall right now how it differed.

The main issue we ran into is that we now have enough code in apps and unit tests that changing this sort of core API takes a huge effort and ended up becoming impractical. Have you evaluated the cost of the change? We’re running out of time to do such things.