Skip to content

Commit

Permalink
Allow transactions to explicitly specify mutual exclusion. (#349)
Browse files Browse the repository at this point in the history
It is oftentimes nice to leave the fee field unconstrained (but capped at some
max value) so that a transaction submitter can later pick a fee appropriately at
submission time.  However, for certain TEAL program applications, this leaves
the possibility of replay open.

This commit adds a new transaction field, Lease, which allows transactions to
specify that they are mutually exclusive with other transactions.  If this field
is nonzero in a transaction, then once the transaction is confirmed, it holds
the lock identified by the (Sender, Lease) pair of the transaction until the
LastValid round passes. While this transaction possesses the lock, no other
transaction specifying this lock can be confirmed.
  • Loading branch information
derbear committed Sep 25, 2019
1 parent e48332f commit dad58c7
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 34 deletions.
4 changes: 4 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,9 @@ type ConsensusParams struct {

// max group size
MaxTxGroupSize int

// support for transaction leases
SupportTransactionLeases bool
}

// Consensus tracks the protocol-level settings for different versions of the
Expand Down Expand Up @@ -412,6 +415,7 @@ func initConsensusProtocols() {
vFuture.MaxAssetsPerAccount = 1000
vFuture.SupportTxGroups = true
vFuture.MaxTxGroupSize = 16
vFuture.SupportTransactionLeases = true
vFuture.SupportBecomeNonParticipatingTransactions = true
vFuture.ApprovedUpgrades = map[protocol.ConsensusVersion]bool{}
Consensus[protocol.ConsensusFuture] = vFuture
Expand Down
10 changes: 10 additions & 0 deletions data/transactions/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,13 @@ type Header struct {
// transaction group (and, if so, specifies the hash
// of a TxGroup).
Group crypto.Digest `codec:"grp"`

// Lease enforces mutual exclusion of transactions. If this field is
// nonzero, then once the transaction is confirmed, it acquires the
// lease identified by the (Sender, Lease) pair of the transaction until
// the LastValid round passes. While this transaction possesses the
// lease, no other transaction specifying this lease can be confirmed.
Lease [32]byte `codec:"lx"`
}

// Transaction describes a transaction that can appear in a block.
Expand Down Expand Up @@ -330,6 +337,9 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa
// this check is just to be safe, but reaching here seems impossible, since it requires computing a preimage of rwpool
return fmt.Errorf("transaction from incentive pool is invalid")
}
if !proto.SupportTransactionLeases && (tx.Lease != [32]byte{}) {
return fmt.Errorf("transaction tried to acquire lease %v but protocol does not support transaction leases", tx.Lease)
}
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion data/txHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ func (handler *TxHandler) checkAlreadyCommitted(tx *txBacklogMsg) (processingDon
return true
}

committed, err := handler.ledger.Committed(txn)
committed, err := handler.ledger.Committed(tx.proto, txn)
if err != nil {
logging.Base().Errorf("Could not verify committed status of txn %v: %v", txn, err)
return true
Expand Down
38 changes: 27 additions & 11 deletions ledger/cow.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import (

type roundCowParent interface {
lookup(basics.Address) (basics.AccountData, error)
isDup(basics.Round, transactions.Txid) (bool, error)
isDup(basics.Round, transactions.Txid, txlease) (bool, error)
txnCounter() uint64
}

Expand All @@ -52,6 +52,9 @@ type stateDelta struct {
// new Txids for the txtail and TxnCounter
txids map[transactions.Txid]struct{}

// new txleases for the txtail mapped to expiration
txleases map[txlease]basics.Round

// new block header; read-only
hdr *bookkeeping.BlockHeader
}
Expand All @@ -62,9 +65,10 @@ func makeRoundCowState(b roundCowParent, hdr bookkeeping.BlockHeader) *roundCowS
commitParent: nil,
proto: config.Consensus[hdr.CurrentProtocol],
mods: stateDelta{
accts: make(map[basics.Address]accountDelta),
txids: make(map[transactions.Txid]struct{}),
hdr: &hdr,
accts: make(map[basics.Address]accountDelta),
txids: make(map[transactions.Txid]struct{}),
txleases: make(map[txlease]basics.Round),
hdr: &hdr,
},
}
}
Expand All @@ -82,13 +86,20 @@ func (cb *roundCowState) lookup(addr basics.Address) (data basics.AccountData, e
return cb.lookupParent.lookup(addr)
}

func (cb *roundCowState) isDup(firstValid basics.Round, txid transactions.Txid) (bool, error) {
func (cb *roundCowState) isDup(firstValid basics.Round, txid transactions.Txid, txl txlease) (bool, error) {
_, present := cb.mods.txids[txid]
if present {
return true, nil
}

return cb.lookupParent.isDup(firstValid, txid)
if cb.proto.SupportTransactionLeases && (txl.lease != [32]byte{}) {
expires, ok := cb.mods.txleases[txl]
if ok && cb.mods.hdr.Round <= expires {
return true, nil
}
}

return cb.lookupParent.isDup(firstValid, txid, txl)
}

func (cb *roundCowState) txnCounter() uint64 {
Expand All @@ -104,8 +115,9 @@ func (cb *roundCowState) put(addr basics.Address, old basics.AccountData, new ba
}
}

func (cb *roundCowState) addTx(txid transactions.Txid) {
cb.mods.txids[txid] = struct{}{}
func (cb *roundCowState) addTx(txn transactions.Transaction) {
cb.mods.txids[txn.ID()] = struct{}{}
cb.mods.txleases[txlease{sender: txn.Sender, lease: txn.Lease}] = txn.LastValid
}

func (cb *roundCowState) child() *roundCowState {
Expand All @@ -114,9 +126,10 @@ func (cb *roundCowState) child() *roundCowState {
commitParent: cb,
proto: cb.proto,
mods: stateDelta{
accts: make(map[basics.Address]accountDelta),
txids: make(map[transactions.Txid]struct{}),
hdr: cb.mods.hdr,
accts: make(map[basics.Address]accountDelta),
txids: make(map[transactions.Txid]struct{}),
txleases: make(map[txlease]basics.Round),
hdr: cb.mods.hdr,
},
}
}
Expand All @@ -137,6 +150,9 @@ func (cb *roundCowState) commitToParent() {
for txid := range cb.mods.txids {
cb.commitParent.mods.txids[txid] = struct{}{}
}
for txl, expires := range cb.mods.txleases {
cb.commitParent.mods.txleases[txl] = expires
}
}

func (cb *roundCowState) modifiedAccounts() []basics.Address {
Expand Down
2 changes: 1 addition & 1 deletion ledger/cow_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func (ml *mockLedger) lookup(addr basics.Address) (basics.AccountData, error) {
return ml.balanceMap[addr], nil
}

func (ml *mockLedger) isDup(firstValid basics.Round, txn transactions.Txid) (bool, error) {
func (ml *mockLedger) isDup(firstValid basics.Round, txn transactions.Txid, txl txlease) (bool, error) {
return false, nil
}

Expand Down
18 changes: 11 additions & 7 deletions ledger/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,17 @@ type roundCowBase struct {

// TxnCounter from previous block header.
txnCount uint64

// The current protocol consensus params.
proto config.ConsensusParams
}

func (x *roundCowBase) lookup(addr basics.Address) (basics.AccountData, error) {
return x.l.LookupWithoutRewards(x.rnd, addr)
}

func (x *roundCowBase) isDup(firstValid basics.Round, txid transactions.Txid) (bool, error) {
return x.l.isDup(firstValid, x.rnd, txid)
func (x *roundCowBase) isDup(firstValid basics.Round, txid transactions.Txid, txl txlease) (bool, error) {
return x.l.isDup(x.proto, x.rnd+1, firstValid, x.rnd, txid, txl)
}

func (x *roundCowBase) txnCounter() uint64 {
Expand Down Expand Up @@ -167,7 +170,7 @@ type ledgerForEvaluator interface {
BlockHdr(basics.Round) (bookkeeping.BlockHeader, error)
Lookup(basics.Round, basics.Address) (basics.AccountData, error)
Totals(basics.Round) (AccountTotals, error)
isDup(basics.Round, basics.Round, transactions.Txid) (bool, error)
isDup(config.ConsensusParams, basics.Round, basics.Round, basics.Round, transactions.Txid, txlease) (bool, error)
LookupWithoutRewards(basics.Round, basics.Address) (basics.AccountData, error)
}

Expand All @@ -193,7 +196,8 @@ func startEvaluator(l ledgerForEvaluator, hdr bookkeeping.BlockHeader, aux *eval
// the block at this round below, so underflow will be caught.
// If we are not validating, we must have previously checked
// an agreement.Certificate attesting that hdr is valid.
rnd: hdr.Round - 1,
rnd: hdr.Round - 1,
proto: proto,
}

eval := &BlockEvaluator{
Expand Down Expand Up @@ -450,7 +454,7 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti
}

// Transaction already in the ledger?
dup, err := cow.isDup(txn.Txn.First(), txn.ID())
dup, err := cow.isDup(txn.Txn.First(), txn.ID(), txlease{sender: txn.Txn.Sender, lease: txn.Txn.Lease})
if err != nil {
return err
}
Expand Down Expand Up @@ -535,8 +539,8 @@ func (eval *BlockEvaluator) transaction(txn transactions.SignedTxn, ad transacti
}
}

// Remember this TXID (to detect duplicates)
cow.addTx(txn.ID())
// Remember this txn
cow.addTx(txn.Txn)

return nil
}
Expand Down
17 changes: 13 additions & 4 deletions ledger/ledger.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,10 @@ func (l *Ledger) Totals(rnd basics.Round) (AccountTotals, error) {
return l.accts.totals(rnd)
}

func (l *Ledger) isDup(firstValid basics.Round, lastValid basics.Round, txid transactions.Txid) (bool, error) {
func (l *Ledger) isDup(currentProto config.ConsensusParams, current basics.Round, firstValid basics.Round, lastValid basics.Round, txid transactions.Txid, txl txlease) (bool, error) {
l.trackerMu.RLock()
defer l.trackerMu.RUnlock()
return l.txTail.isDup(firstValid, lastValid, txid)
return l.txTail.isDup(currentProto, current, firstValid, lastValid, txid, txl)
}

// Latest returns the latest known block round added to the ledger.
Expand All @@ -325,10 +325,12 @@ func (l *Ledger) LatestCommitted() basics.Round {

// Committed uses the transaction tail tracker to check if txn already
// appeared in a block.
func (l *Ledger) Committed(txn transactions.SignedTxn) (bool, error) {
func (l *Ledger) Committed(currentProto config.ConsensusParams, txn transactions.SignedTxn) (bool, error) {
l.trackerMu.RLock()
defer l.trackerMu.RUnlock()
return l.txTail.isDup(txn.Txn.First(), l.Latest(), txn.ID())
// do not check for whether lease would excluded this
txl := txlease{sender: txn.Txn.Sender}
return l.txTail.isDup(currentProto, l.Latest()+1, txn.Txn.First(), l.Latest(), txn.ID(), txl)
}

func (l *Ledger) blockAux(rnd basics.Round) (bookkeeping.Block, evalAux, error) {
Expand Down Expand Up @@ -454,3 +456,10 @@ func (l *Ledger) trackerEvalVerified(blk bookkeeping.Block, aux evalAux) (stateD
delta, _, err := l.eval(context.Background(), blk, &aux, false, nil, nil)
return delta, err
}

// A txlease is a transaction (sender, lease) pair which uniquely specifies a
// transaction lease.
type txlease struct {
sender basics.Address
lease [32]byte
}
82 changes: 79 additions & 3 deletions ledger/ledger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -522,20 +522,20 @@ func TestLedgerSingleTx(t *testing.T) {
a.Error(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctKeyreg, ad), "added duplicate tx")
}

func TestLedgerSingleTxApplyData(t *testing.T) {
func testLedgerSingleTxApplyData(t *testing.T, version protocol.ConsensusVersion) {
a := require.New(t)

backlogPool := execpool.MakeBacklog(nil, 0, execpool.LowPriority, nil)
defer backlogPool.Shutdown()

genesisInitState, initSecrets := testGenerateInitState(t, protocol.ConsensusCurrentVersion)
genesisInitState, initSecrets := testGenerateInitState(t, version)
const inMem = true
const archival = true
l, err := OpenLedger(logging.Base(), t.Name(), inMem, genesisInitState, archival)
a.NoError(err, "could not open ledger")
defer l.Close()

proto := config.Consensus[protocol.ConsensusCurrentVersion]
proto := config.Consensus[version]
poolAddr := testPoolAddr
sinkAddr := testSinkAddr

Expand Down Expand Up @@ -685,4 +685,80 @@ func TestLedgerSingleTxApplyData(t *testing.T) {
a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctKeyreg, ad), "could not add key registration")

a.Error(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctKeyreg, ad), "added duplicate tx")

leaseReleaseRound := l.Latest() + 10
correctPayLease := correctPay
correctPayLease.Sender = addrList[3]
correctPayLease.Lease[0] = 1
correctPayLease.LastValid = leaseReleaseRound
if proto.SupportTransactionLeases {
a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "could not add payment transaction with payment lease")

correctPayLease.Note = make([]byte, 1)
correctPayLease.Note[0] = 1
correctPayLease.LastValid += 10
a.Error(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "added payment transaction with matching transaction lease")
correctPayLeaseOther := correctPayLease
correctPayLeaseOther.Sender = addrList[4]
a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLeaseOther, ad), "could not add payment transaction with matching lease but different sender")
correctPayLeaseOther = correctPayLease
correctPayLeaseOther.Lease[0]++
a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLeaseOther, ad), "could not add payment transaction with matching sender but different lease")

for l.Latest() < leaseReleaseRound {
a.Error(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "added payment transaction with matching transaction lease")

var totalRewardUnits uint64
for _, acctdata := range initAccounts {
totalRewardUnits += acctdata.MicroAlgos.RewardUnits(proto)
}
poolBal, err := l.Lookup(l.Latest(), testPoolAddr)
a.NoError(err, "could not get incentive pool balance")
var emptyPayset transactions.Payset
lastBlock, err := l.Block(l.Latest())
a.NoError(err, "could not get last block")

correctHeader := bookkeeping.BlockHeader{
GenesisID: t.Name(),
Round: l.Latest() + 1,
Branch: lastBlock.Hash(),
TxnRoot: emptyPayset.Commit(proto.PaysetCommitFlat),
TimeStamp: 0,
RewardsState: lastBlock.NextRewardsState(l.Latest()+1, proto, poolBal.MicroAlgos, totalRewardUnits),
UpgradeState: lastBlock.UpgradeState,
// Seed: does not matter,
// UpgradeVote: empty,
}
correctHeader.RewardsPool = testPoolAddr
correctHeader.FeeSink = testSinkAddr

if proto.SupportGenesisHash {
correctHeader.GenesisHash = crypto.Hash([]byte(t.Name()))
}

if proto.TxnCounter {
correctHeader.TxnCounter = lastBlock.TxnCounter
}

correctBlock := bookkeeping.Block{BlockHeader: correctHeader}
a.NoError(l.appendUnvalidated(correctBlock), "could not add block with correct header")
}

a.NoError(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "could not add payment transaction after lease was dropped")
} else {
a.Error(l.appendUnvalidatedTx(t, initAccounts, initSecrets, correctPayLease, ad), "added payment transaction with transaction lease unsupported by protocol version")
}
}

func TestLedgerSingleTxApplyData(t *testing.T) {
testLedgerSingleTxApplyData(t, protocol.ConsensusCurrentVersion)
}

// SupportTransactionLeases was introduced after v18.
func TestLedgerSingleTxApplyDataV18(t *testing.T) {
testLedgerSingleTxApplyData(t, protocol.ConsensusV18)
}

func TestLedgerSingleTxApplyDataFuture(t *testing.T) {
testLedgerSingleTxApplyData(t, protocol.ConsensusFuture)
}
Loading

0 comments on commit dad58c7

Please sign in to comment.