Skip to content

Commit 3a4a37f

Browse files
authored
pool fees in a txgroup (#2173)
A fair amount of complexity is introduced in transaction groups when the goal is to let some entity perform an action at the expense of another. For example, a contract account might be willing to perform an exchange, but expects the caller to compensate it to replace the fee that the contract account must pay. This changes fee accounting to simplify these situations. Rather than check that each txn in a txn group meets the min fee, the txgroup is checked as a whole to ensure the total fee exceeds n*min_fee for an n member txgroup.
1 parent 23bfcd7 commit 3a4a37f

File tree

7 files changed

+101
-7
lines changed

7 files changed

+101
-7
lines changed

cmd/goal/clerk.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,14 @@ var sendCmd = &cobra.Command{
375375
payment.RekeyTo = rekeyTo
376376
}
377377

378+
// ConstructPayment fills in the suggested fee when fee=0. But if the user actually used --fee=0 on the
379+
// commandline, we ought to do what they asked (especially now that zero or low fees make sense in
380+
// combination with other txns that cover the groups's fee.
381+
explicitFee := cmd.Flags().Changed("fee")
382+
if explicitFee {
383+
payment.Fee = basics.MicroAlgos{Raw: fee}
384+
}
385+
378386
var stx transactions.SignedTxn
379387
if lsig.Logic != nil {
380388

config/consensus.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ type ConsensusParams struct {
103103
// a way of making the spender subsidize the cost of storing this transaction.
104104
MinTxnFee uint64
105105

106+
// EnableFeePooling specifies that the sum of the fees in a
107+
// group must exceed one MinTxnFee per Txn, rather than check that
108+
// each Txn has a MinFee.
109+
EnableFeePooling bool
110+
106111
// RewardUnit specifies the number of MicroAlgos corresponding to one reward
107112
// unit.
108113
//
@@ -944,6 +949,8 @@ func initConsensusProtocols() {
944949
// Increase asset URL length to allow for IPFS URLs
945950
vFuture.MaxAssetURLBytes = 96
946951

952+
vFuture.EnableFeePooling = true
953+
947954
Consensus[protocol.ConsensusFuture] = vFuture
948955
}
949956

data/transactions/transaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -472,7 +472,7 @@ func (tx Transaction) WellFormed(spec SpecialAddresses, proto config.ConsensusPa
472472
}
473473
}
474474

475-
if tx.Fee.LessThan(basics.MicroAlgos{Raw: proto.MinTxnFee}) {
475+
if !proto.EnableFeePooling && tx.Fee.LessThan(basics.MicroAlgos{Raw: proto.MinTxnFee}) {
476476
if tx.Type == protocol.CompactCertTx {
477477
// Zero fee allowed for compact cert txn.
478478
} else {

data/transactions/verify/txn.go

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,6 @@ func Txn(s *transactions.SignedTxn, txnIdx int, groupCtx *GroupContext) error {
108108
return err
109109
}
110110

111-
if s.Txn.Src().IsZero() {
112-
return errors.New("empty address")
113-
}
114-
115111
return stxnVerifyCore(s, txnIdx, groupCtx)
116112
}
117113

@@ -121,13 +117,34 @@ func TxnGroup(stxs []transactions.SignedTxn, contextHdr bookkeeping.BlockHeader,
121117
if err != nil {
122118
return nil, err
123119
}
120+
121+
minFeeCount := uint64(0)
122+
feesPaid := uint64(0)
124123
for i, stxn := range stxs {
125124
err = Txn(&stxn, i, groupCtx)
126125
if err != nil {
127126
err = fmt.Errorf("transaction %+v invalid : %w", stxn, err)
128127
return
129128
}
129+
if stxn.Txn.Type != protocol.CompactCertTx {
130+
minFeeCount++
131+
}
132+
feesPaid = basics.AddSaturate(feesPaid, stxn.Txn.Fee.Raw)
130133
}
134+
feeNeeded, overflow := basics.OMul(groupCtx.consensusParams.MinTxnFee, minFeeCount)
135+
if overflow {
136+
err = fmt.Errorf("txgroup fee requirement overflow")
137+
return
138+
}
139+
// feesPaid may have saturated. That's ok. Since we know
140+
// feeNeeded did not overlfow, simple comparision tells us
141+
// feesPaid was enough.
142+
if feesPaid < feeNeeded {
143+
err = fmt.Errorf("txgroup had %d in fees, which is less than the minimum %d * %d",
144+
feesPaid, minFeeCount, groupCtx.consensusParams.MinTxnFee)
145+
return
146+
}
147+
131148
if cache != nil {
132149
cache.Add(stxs, groupCtx)
133150
}

ledger/apply/payment_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ func TestPaymentValidation(t *testing.T) {
221221
if badFee.WellFormed(spec, tc.Proto) == nil {
222222
t.Errorf("transaction with no fee %#v verified incorrectly", badFee)
223223
}
224+
badFee.Fee.Raw = 1
225+
if badFee.WellFormed(spec, tc.Proto) == nil {
226+
t.Errorf("transaction with low fee %#v verified incorrectly", badFee)
227+
}
228+
224229
}
225230
}
226231

test/e2e-go/cli/goal/clerk_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,14 @@ func TestClerkSendNoteEncoding(t *testing.T) {
4444
account := accounts[0].Address
4545

4646
const noteText = "Sample Text-based Note"
47-
txID, err := fixture.ClerkSend(account, account, 100, 1, noteText)
47+
txID, err := fixture.ClerkSend(account, account, 100, 1000, noteText)
4848
a.NoError(err)
4949
a.NotEmpty(txID)
5050

5151
// Send 2nd txn using the note encoded as base-64 (using --noteb64)
5252
originalNoteb64Text := "Noteb64-encoded text With Binary \u0001x1x0x3"
5353
noteb64 := base64.StdEncoding.EncodeToString([]byte(originalNoteb64Text))
54-
txID2, err := fixture.ClerkSendNoteb64(account, account, 100, 1, noteb64)
54+
txID2, err := fixture.ClerkSendNoteb64(account, account, 100, 1000, noteb64)
5555
a.NoError(err)
5656
a.NotEmpty(txID2)
5757

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/bin/bash
2+
3+
filename=$(basename "$0")
4+
scriptname="${filename%.*}"
5+
date "+${scriptname} start %Y%m%d_%H%M%S"
6+
7+
set -e
8+
set -x
9+
set -o pipefail
10+
export SHELLOPTS
11+
12+
WALLET=$1
13+
14+
gcmd="goal -w ${WALLET}"
15+
16+
PAYER=$(${gcmd} account list|awk '{ print $3 }')
17+
MOOCHER=$(${gcmd} account new|awk '{ print $6 }')
18+
19+
# Fund MOOCHER, 1M
20+
${gcmd} clerk send -a 1000000 -f "${PAYER}" -t "${MOOCHER}"
21+
22+
# Payer and Moocher are going to pay each other 100 mAlgos, but
23+
# Moocher is not going to pay the minfee (Payer will pay double)
24+
25+
# Fair number of temporary files, just cd into TEMPDIR first
26+
cd ${TEMPDIR}
27+
28+
# Check a low fee from moocher
29+
${gcmd} clerk send -a 100 -f "${MOOCHER}" -t "${PAYER}" --fee 2 -o cheap.txn
30+
# Since goal was modified to allow < minfee when this feature was added, let's confirm
31+
msgpacktool -d < cheap.txn | grep fee | grep 2
32+
${gcmd} clerk send -a 100 -f "${PAYER}" -t "${MOOCHER}" --fee 2000 -o expensive.txn
33+
cat cheap.txn expensive.txn > both.txn
34+
${gcmd} clerk group -i both.txn -o group.txn
35+
${gcmd} clerk sign -i group.txn -o group.stx
36+
${gcmd} clerk rawsend -f group.stx
37+
38+
# Check a zero fee from moocher
39+
${gcmd} clerk send -a 100 -f "${MOOCHER}" -t "${PAYER}" --fee 0 -o cheap.txn
40+
# Since goal was modified to allow zero when this feature was added, let's confirm
41+
# that it's not encoded (should be "omitempty")
42+
set +e
43+
FOUND=$(msgpacktool -d < cheap.txn | grep fee)
44+
set -e
45+
if [[ $FOUND != "" ]]; then
46+
date "+{scriptname} FAIL fee was improperly encoded $FOUND %Y%m%d_%H%M%S"
47+
false
48+
fi
49+
50+
${gcmd} clerk send -a 100 -f "${PAYER}" -t "${MOOCHER}" --fee 2000 -o expensive.txn
51+
cat cheap.txn expensive.txn > both.txn
52+
${gcmd} clerk group -i both.txn -o group.txn
53+
${gcmd} clerk sign -i group.txn -o group.stx
54+
${gcmd} clerk rawsend -f group.stx
55+
56+
57+
date "+${scriptname} OK %Y%m%d_%H%M%S"

0 commit comments

Comments
 (0)