Repository: hashgraph/hedera-services
Issue: Node payments for a scheduled txn should go to node submitting the triggering txn 

**Summary of the defect**
`ItemizableFeeCharging` uses the [node account in the `TransactionBody`](https://github.com/hashgraph/hedera-services/blob/master/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java#L97) to identify where to transfer node payments.

This is always correct for a normal transaction, but for a scheduled transaction this account should be ignored; in fact, the payer for a triggered transaction **does not owe any node fees**.

**Suggested fix**
- In `ItemizableFeeCharging` only use `submittingMember` for the node account, since this is always identical to the node account from the transaction body when `NODE` fees are being paid.
- Add a `TxnFeeChargingPolicy.applyForTriggered()` method which discounts the node fee to 0. (This is totally analogous to `TxnFeeChargingPolicy.applyForDuplicate()` which discounts the service fee to 0.)

Changed files:
hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java
test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java
hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java
hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java
test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java
test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/schedule/HapiScheduleCreate.java
test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java
test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java
hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java
hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java
test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java

diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java
index 272dae19de..9884fb118a 100644
--- a/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java
+++ b/hedera-node/src/main/java/com/hedera/services/fees/charging/ItemizableFeeCharging.java
@@ -52,13 +52,13 @@ import static com.hedera.services.fees.TxnFeeType.*;
 public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements TxnScopedFeeCharging {
 	public static EnumSet<TxnFeeType> NODE_FEE = EnumSet.of(NODE);
 	public static EnumSet<TxnFeeType> NETWORK_FEE = EnumSet.of(NETWORK);
+	public static EnumSet<TxnFeeType> SERVICE_FEE = EnumSet.of(SERVICE);
 	public static EnumSet<TxnFeeType> NETWORK_NODE_SERVICE_FEES = EnumSet.of(NETWORK, NODE, SERVICE);
 
 	private HederaLedger ledger;
 
 	private final GlobalDynamicProperties properties;
 
-	AccountID node;
 	AccountID funding;
 	AccountID submittingNode;
 	EnumMap<TxnFeeType, Long> payerFeesCharged = new EnumMap<>(TxnFeeType.class);
@@ -94,7 +94,6 @@ public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements T
 	public void resetFor(TxnAccessor accessor, AccountID submittingNode) {
 		super.resetFor(accessor);
 
-		node = accessor.getTxn().getNodeAccountID();
 		funding = properties.fundingAccount();
 		this.submittingNode = submittingNode;
 
@@ -120,11 +119,10 @@ public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements T
 	public TransferList itemizedFees() {
 		TransferList.Builder fees = TransferList.newBuilder();
 
-		if (!submittingNodeFeesCharged.isEmpty()) {
+		AccountID payer = accessor.getPayer();
+		if (!payer.equals(submittingNode) && !submittingNodeFeesCharged.isEmpty()) {
 			includeIfCharged(NETWORK, submittingNode, submittingNodeFeesCharged, fees);
 		} else {
-			AccountID payer = accessor.getPayer();
-
 			includeIfCharged(NETWORK, payer, payerFeesCharged, fees);
 			includeIfCharged(NODE, payer, payerFeesCharged, fees);
 			includeIfCharged(SERVICE, payer, payerFeesCharged, fees);
@@ -140,7 +138,7 @@ public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements T
 			TransferList.Builder fees
 	) {
 		if (feesCharged.containsKey(fee)) {
-			AccountID receiver = (fee == NODE) ? node : funding;
+			AccountID receiver = (fee == NODE) ? submittingNode : funding;
 			fees.addAllAccountAmounts(receiverFirst(source, receiver, feesCharged.get(fee)));
 		}
 	}
@@ -177,7 +175,7 @@ public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements T
 	public void chargePayerUpTo(EnumSet<TxnFeeType> fees) {
 		pay(
 				fees,
-				() -> chargeUpTo(accessor.getPayer(), node, NODE),
+				() -> chargeUpTo(accessor.getPayer(), submittingNode, NODE),
 				(fee) -> chargeUpTo(accessor.getPayer(), funding, fee));
 	}
 
@@ -185,7 +183,7 @@ public class ItemizableFeeCharging extends FieldSourcedFeeScreening implements T
 	public void chargeParticipant(AccountID participant, EnumSet<TxnFeeType> fees) {
 		pay(
 				fees,
-				() -> charge(participant, node, NODE),
+				() -> charge(participant, submittingNode, NODE),
 				fee -> charge(participant, funding, fee));
 	}
 
diff --git a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java
index 3b2758c922..428101e4b7 100644
--- a/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java
+++ b/hedera-node/src/main/java/com/hedera/services/fees/charging/TxnFeeChargingPolicy.java
@@ -31,18 +31,20 @@ import static com.hedera.services.fees.TxnFeeType.SERVICE;
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_FEE;
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_NODE_SERVICE_FEES;
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NODE_FEE;
+import static com.hedera.services.fees.charging.ItemizableFeeCharging.SERVICE_FEE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK;
 
 /**
  * Provides the transaction fee-charging policy for the processing
- * logic. The policy offers three basic entry points:
+ * logic. The policy offers four basic entry points:
  * <ol>
  *    <li>For a txn whose submitting node seemed to ignore due diligence
  *    (e.g. submitted a txn with an impermissible valid duration); and, </li>
  *    <li>For a txn that looks to have been submitted responsibly, but is
  *    a duplicate of a txn already submitted by a different node; and,</li>
+ *    <li>For a triggered txn; and,</li>
  *    <li>For a txn that was submitted responsibly, and is believed unique.</li>
  * </ol>
  *
@@ -64,6 +66,27 @@ public class TxnFeeChargingPolicy {
 		return applyWithDiscount(charging, fee, NO_DISCOUNT);
 	}
 
+	/**
+	 * Apply the fee charging policy to a txn that was submitted responsibly, but
+	 * is a triggered txn rather than a parent txn requiring node precheck work.
+	 *
+	 * @param charging the charging facility to use
+	 * @param fee the fee to charge
+	 * @return the outcome of applying the policy
+	 */
+	public ResponseCodeEnum applyForTriggered(ItemizableFeeCharging charging, FeeObject fee) {
+		charging.setFor(SERVICE, fee.getServiceFee());
+
+		if (!charging.isPayerWillingToCover(SERVICE_FEE)) {
+			return INSUFFICIENT_TX_FEE;
+		} else if (!charging.canPayerAfford(SERVICE_FEE)) {
+			return INSUFFICIENT_PAYER_BALANCE;
+		} else {
+			charging.chargePayer(SERVICE_FEE);
+			return OK;
+		}
+	}
+
 	/**
 	 * Apply the fee charging policy to a txn that was submitted responsibly, but
 	 * is a duplicate of a txn already submitted by a different node.
@@ -112,6 +135,7 @@ public class TxnFeeChargingPolicy {
 			ItemizableFeeCharging charging,
 			Consumer<ItemizableFeeCharging> discount
 	) {
+		discount.accept(charging);
 		if (!charging.isPayerWillingToCover(NETWORK_NODE_SERVICE_FEES))	{
 			penalizePayer(charging);
 			return INSUFFICIENT_TX_FEE;
@@ -119,7 +143,6 @@ public class TxnFeeChargingPolicy {
 			penalizePayer(charging);
 			return INSUFFICIENT_PAYER_BALANCE;
 		} else {
-			discount.accept(charging);
 			charging.chargePayer(NETWORK_NODE_SERVICE_FEES);
 			return OK;
 		}
diff --git a/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java b/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java
index 197c37176f..1e6d67ab2d 100644
--- a/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java
+++ b/hedera-node/src/main/java/com/hedera/services/legacy/services/state/AwareProcessLogic.java
@@ -188,7 +188,7 @@ public class AwareProcessLogic implements ProcessLogic {
 
 		FeeObject fee = ctx.fees().computeFee(accessor, ctx.txnCtx().activePayerKey(), ctx.currentView());
 
-		var chargingOutcome = ctx.txnChargingPolicy().apply(ctx.charging(), fee);
+		var chargingOutcome = ctx.txnChargingPolicy().applyForTriggered(ctx.charging(), fee);
 		if (chargingOutcome != OK) {
 			ctx.txnCtx().setStatus(chargingOutcome);
 			return;
diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java
index 5ac9969715..a13b51c8fd 100644
--- a/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java
+++ b/hedera-node/src/test/java/com/hedera/services/fees/charging/ItemizableFeeChargingTest.java
@@ -34,7 +34,6 @@ import org.junit.jupiter.api.Test;
 
 import java.util.EnumMap;
 import java.util.EnumSet;
-import java.util.Set;
 
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_NODE_SERVICE_FEES;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -71,7 +70,7 @@ class ItemizableFeeChargingTest {
 		exemptions = mock(FeeExemptions.class);
 		properties = mock(GlobalDynamicProperties.class);
 
-		given(txn.getNodeAccountID()).willReturn(givenNode);
+		given(txn.getNodeAccountID()).willThrow(IllegalStateException.class);
 		given(accessor.getTxn()).willReturn(txn);
 		given(accessor.getPayer()).willReturn(payer);
 		given(properties.fundingAccount()).willReturn(funding);
@@ -122,7 +121,7 @@ class ItemizableFeeChargingTest {
 	@Test
 	public void doesntRecordSelfPayments() {
 		givenKnownFeeAmounts();
-		given(accessor.getPayer()).willReturn(givenNode);
+		given(accessor.getPayer()).willReturn(submittingNode);
 
 		// when:
 		subject.chargePayer(EnumSet.of(NODE));
@@ -213,7 +212,7 @@ class ItemizableFeeChargingTest {
 	public void itemizesWhenNodeIsPayer() {
 		givenKnownFeeAmounts();
 		given(ledger.getBalance(any())).willReturn(Long.MAX_VALUE);
-		given(accessor.getPayer()).willReturn(givenNode);
+		given(accessor.getPayer()).willReturn(submittingNode);
 
 		// when:
 		subject.chargePayer(NETWORK_NODE_SERVICE_FEES);
@@ -225,9 +224,9 @@ class ItemizableFeeChargingTest {
 				itemizedFees.getAccountAmountsList(),
 				contains(
 						aa(funding, network),
-						aa(givenNode, -network),
+						aa(submittingNode, -network),
 						aa(funding, service),
-						aa(givenNode, -service)));
+						aa(submittingNode, -service)));
 	}
 
 	@Test
@@ -246,7 +245,7 @@ class ItemizableFeeChargingTest {
 				contains(
 						aa(funding, network),
 						aa(payer, -network),
-						aa(givenNode, node),
+						aa(submittingNode, node),
 						aa(payer, -node),
 						aa(funding, service),
 						aa(payer, -service)));
@@ -266,7 +265,7 @@ class ItemizableFeeChargingTest {
 		// then:
 		verify(ledger).doTransfer(participant, funding, network);
 		verify(ledger).doTransfer(participant, funding, service);
-		verify(ledger).doTransfer(participant, givenNode, node);
+		verify(ledger).doTransfer(participant, submittingNode, node);
 		// and:
 		assertTrue(subject.submittingNodeFeesCharged.isEmpty());
 		assertTrue(subject.payerFeesCharged.isEmpty());
@@ -282,7 +281,7 @@ class ItemizableFeeChargingTest {
 		// then:
 		verify(ledger).doTransfer(payer, funding, network);
 		verify(ledger).doTransfer(payer, funding, service);
-		verify(ledger).doTransfer(payer, givenNode, node);
+		verify(ledger).doTransfer(payer, submittingNode, node);
 		// and:
 		assertEquals(network, subject.payerFeesCharged.get(NETWORK).longValue());
 		assertEquals(service, subject.payerFeesCharged.get(SERVICE).longValue());
@@ -301,7 +300,7 @@ class ItemizableFeeChargingTest {
 
 		// then:
 		verify(ledger).doTransfer(payer, funding, network);
-		verify(ledger).doTransfer(payer, givenNode, node / 2);
+		verify(ledger).doTransfer(payer, submittingNode, node / 2);
 		// and:
 		assertEquals(network, subject.payerFeesCharged.get(NETWORK).longValue());
 		assertEquals(node / 2, subject.payerFeesCharged.get(NODE).longValue());
diff --git a/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java b/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java
index 454eb3c55a..c75d76f4e3 100644
--- a/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java
+++ b/hedera-node/src/test/java/com/hedera/services/fees/charging/TxnFeeChargingPolicyTest.java
@@ -39,6 +39,7 @@ import static com.hedera.services.fees.TxnFeeType.SERVICE;
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_FEE;
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NETWORK_NODE_SERVICE_FEES;
 import static com.hedera.services.fees.charging.ItemizableFeeCharging.NODE_FEE;
+import static com.hedera.services.fees.charging.ItemizableFeeCharging.SERVICE_FEE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.OK;
@@ -100,7 +101,44 @@ class TxnFeeChargingPolicyTest {
 	}
 
 	@Test
-	public void liveFireDiscountWorks() {
+	public void liveFireWorksForTriggered() {
+		// setup:
+		TransactionBody txn = mock(TransactionBody.class);
+		AccountID submittingNode = IdUtils.asAccount("0.0.3");
+		AccountID payer = IdUtils.asAccount("0.0.1001");
+		AccountID funding = IdUtils.asAccount("0.0.98");
+		HederaLedger ledger = mock(HederaLedger.class);
+		GlobalDynamicProperties properties = mock(GlobalDynamicProperties.class);
+		SignedTxnAccessor accessor = mock(SignedTxnAccessor.class);
+		charging = new ItemizableFeeCharging(ledger, new NoExemptions(), properties);
+
+		given(ledger.getBalance(any())).willReturn(Long.MAX_VALUE);
+		given(properties.fundingAccount()).willReturn(funding);
+		given(txn.getTransactionFee()).willReturn(10L);
+		given(accessor.getTxn()).willReturn(txn);
+
+		given(accessor.getPayer()).willReturn(payer);
+
+		// when:
+		charging.resetFor(accessor, submittingNode);
+		ResponseCodeEnum outcome = subject.applyForTriggered(charging, fee);
+
+		// then:
+		verify(ledger).doTransfer(payer, funding, service);
+		verify(ledger, never()).doTransfer(
+				argThat(payer::equals),
+				argThat(funding::equals),
+				longThat(l -> l == network));
+		verify(ledger, never()).doTransfer(
+				argThat(payer::equals),
+				argThat(submittingNode::equals),
+				longThat(l -> l == node));
+		// and:
+		assertEquals(OK, outcome);
+	}
+
+	@Test
+	public void liveFireDiscountWorksForDuplicate() {
 		// setup:
 		TransactionBody txn = mock(TransactionBody.class);
 		AccountID submittingNode = IdUtils.asAccount("0.0.3");
@@ -207,6 +245,38 @@ class TxnFeeChargingPolicyTest {
 		assertEquals(INSUFFICIENT_TX_FEE, outcome);
 	}
 
+	@Test
+	public void requiresWillingToPayServiceWhenTriggeredTxn() {
+		given(charging.isPayerWillingToCover(SERVICE_FEE)).willReturn(false);
+
+		// when:
+		ResponseCodeEnum outcome = subject.applyForTriggered(charging, fee);
+
+		// then:
+		verify(charging).setFor(SERVICE, service);
+		// and:
+		verify(charging).isPayerWillingToCover(SERVICE_FEE);
+		// and:
+		assertEquals(INSUFFICIENT_TX_FEE, outcome);
+	}
+
+	@Test
+	public void requiresAbleToPayServiceWhenTriggeredTxn() {
+		given(charging.isPayerWillingToCover(SERVICE_FEE)).willReturn(true);
+		given(charging.canPayerAfford(SERVICE_FEE)).willReturn(false);
+
+		// when:
+		ResponseCodeEnum outcome = subject.applyForTriggered(charging, fee);
+
+		// then:
+		verify(charging).setFor(SERVICE, service);
+		// and:
+		verify(charging).isPayerWillingToCover(SERVICE_FEE);
+		verify(charging).canPayerAfford(SERVICE_FEE);
+		// and:
+		assertEquals(INSUFFICIENT_PAYER_BALANCE, outcome);
+	}
+
 	@Test
 	public void chargesNodePenaltyForPayerUnableToPayNetwork() {
 		given(charging.isPayerWillingToCover(NETWORK_FEE)).willReturn(true);
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java
index e7cac897e1..a9ff3c8742 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/HapiSpecOperation.java
@@ -95,6 +95,7 @@ public abstract class HapiSpecOperation {
 	protected boolean omitTxnId = false;
 	protected boolean loggingOff = false;
 	protected boolean suppressStats = false;
+	protected boolean omitNodeAccount = false;
 	protected boolean verboseLoggingOn = false;
 	protected boolean shouldRegisterTxn = false;
 	protected boolean useDefaultTxnAsCostAnswerPayment = false;
@@ -262,7 +263,11 @@ public abstract class HapiSpecOperation {
 				builder.getTransactionIDBuilder().setNonce(ByteString.copyFrom(nonce));
 			}
 
-			node.ifPresent(builder::setNodeAccountID);
+			if (omitNodeAccount) {
+				builder.clearNodeAccountID();
+			} else {
+				node.ifPresent(builder::setNodeAccountID);
+			}
 			validDurationSecs.ifPresent(s -> {
 				builder.setTransactionValidDuration(Duration.newBuilder().setSeconds(s).build());
 			});
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java
index 6d221d422f..804e9c049d 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/assertions/TransferListAsserts.java
@@ -9,9 +9,9 @@ package com.hedera.services.bdd.spec.assertions;
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- * 
+ *
  *      http://www.apache.org/licenses/LICENSE-2.0
- * 
+ *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -43,24 +43,34 @@ import static com.hedera.services.bdd.spec.transactions.TxnUtils.readableTransfe
 import static java.util.stream.Collectors.toSet;
 
 public class TransferListAsserts extends BaseErroringAssertsProvider<TransferList> {
+	public static TransferListAsserts exactParticipants(Function<HapiApiSpec, List<AccountID>> provider) {
+		return new ExactParticipantsAssert(provider);
+	}
+
 	public static TransferListAsserts including(Function<HapiApiSpec, TransferList>... providers) {
 		return new ExplicitTransferAsserts(Arrays.asList(providers));
 	}
+
 	public static TransferListAsserts includingDeduction(LongSupplier from, long amount) {
 		return new DeductionAsserts(from, amount);
 	}
+
 	public static TransferListAsserts includingDeduction(String desc, String payer) {
 		return new QualifyingDeductionAssert(desc, payer);
 	}
+
 	public static TransferListAsserts atLeastOneTransfer() {
 		return new NonEmptyTransferAsserts();
 	}
+
 	public static TransferListAsserts missingPayments(Function<HapiApiSpec, Map.Entry<AccountID, Long>>... providers) {
 		return new MissingPaymentAsserts(providers);
 	}
+
 	public static Function<HapiApiSpec, Map.Entry<AccountID, Long>> to(String account, Long amount) {
 		return spec -> new AbstractMap.SimpleEntry<>(spec.registry().getAccountID(account), amount);
 	}
+
 	public static Function<HapiApiSpec, Map.Entry<AccountID, Long>> from(String account, Long amount) {
 		return spec -> new AbstractMap.SimpleEntry<>(spec.registry().getAccountID(account), -1 * amount);
 	}
@@ -77,10 +87,10 @@ public class TransferListAsserts extends BaseErroringAssertsProvider<TransferLis
 class MissingPaymentAsserts extends TransferListAsserts {
 	public MissingPaymentAsserts(Function<HapiApiSpec, Map.Entry<AccountID, Long>>... providers) {
 		registerProvider((spec, o) -> {
-			TransferList actual = (TransferList)o;
+			TransferList actual = (TransferList) o;
 			Set<String> missing = Stream.of(providers).map(provider -> asSig(provider.apply(spec))).collect(toSet());
 			Set<String> nonAbsent = new HashSet<>();
-			actual.getAccountAmountsList().stream().forEach(entry ->  {
+			actual.getAccountAmountsList().stream().forEach(entry -> {
 				String sig = asSig(new AbstractMap.SimpleEntry<>(entry.getAccountID(), entry.getAmount()));
 				if (missing.contains(sig)) {
 					nonAbsent.add(sig);
@@ -100,12 +110,27 @@ class MissingPaymentAsserts extends TransferListAsserts {
 	}
 }
 
+class ExactParticipantsAssert extends TransferListAsserts {
+	public ExactParticipantsAssert(Function<HapiApiSpec, List<AccountID>> provider) {
+		registerProvider((spec, o) -> {
+			List<AccountID> expectedParticipants = provider.apply(spec);
+			TransferList actual = (TransferList) o;
+			Assert.assertEquals("Wrong number of participants!",
+					expectedParticipants.size(),
+					actual.getAccountAmountsCount());
+			for (int i = 0, n = expectedParticipants.size(); i < n; i++) {
+				Assert.assertEquals(expectedParticipants.get(i), actual.getAccountAmounts(i).getAccountID());
+			}
+		});
+	}
+}
+
 class ExplicitTransferAsserts extends TransferListAsserts {
 	public ExplicitTransferAsserts(List<Function<HapiApiSpec, TransferList>> providers) {
 		providers.stream().forEach(provider -> {
 			registerProvider((spec, o) -> {
 				TransferList expected = provider.apply(spec);
-				assertInclusion(expected, (TransferList)o);
+				assertInclusion(expected, (TransferList) o);
 			});
 		});
 	}
@@ -114,7 +139,7 @@ class ExplicitTransferAsserts extends TransferListAsserts {
 class QualifyingDeductionAssert extends TransferListAsserts {
 	public QualifyingDeductionAssert(String desc, String payer) {
 		registerProvider((spec, o) -> {
-			var transfers = (TransferList)o;
+			var transfers = (TransferList) o;
 			var hasQualifying = getDeduction(transfers, asId(payer, spec)).isPresent();
 			if (!hasQualifying) {
 				Assert.fail("No qualifying " + desc + " from " + payer + " in " + readableTransferList(transfers));
@@ -126,7 +151,7 @@ class QualifyingDeductionAssert extends TransferListAsserts {
 class NonEmptyTransferAsserts extends TransferListAsserts {
 	public NonEmptyTransferAsserts() {
 		registerProvider((spec, o) -> {
-			TransferList transfers = (TransferList)o;
+			TransferList transfers = (TransferList) o;
 			Assert.assertTrue("Transfer list cannot be empty!", !transfers.getAccountAmountsList().isEmpty());
 		});
 	}
@@ -135,7 +160,7 @@ class NonEmptyTransferAsserts extends TransferListAsserts {
 class DeductionAsserts extends TransferListAsserts {
 	public DeductionAsserts(LongSupplier from, long amount) {
 		registerProvider((sepc, o) -> {
-			TransferList transfers = (TransferList)o;
+			TransferList transfers = (TransferList) o;
 			long num = from.getAsLong();
 			Assert.assertTrue(
 					String.format("No deduction of -%d tinyBars from 0.0.%d detected!", amount, num),
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java
index 0fdb9785cd..252e9fe366 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/queries/meta/HapiGetTxnRecord.java
@@ -27,10 +27,7 @@ import com.hedera.services.bdd.spec.assertions.ErroringAsserts;
 import com.hedera.services.bdd.spec.assertions.ErroringAssertsProvider;
 import com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts;
 import com.hedera.services.bdd.spec.queries.HapiQueryOp;
-import com.hedera.services.bdd.spec.queries.QueryVerbs;
 import com.hedera.services.bdd.spec.transactions.TxnUtils;
-import com.hedera.services.bdd.spec.transactions.schedule.HapiScheduleCreate;
-import com.hedera.services.bdd.spec.utilops.CustomSpecAssert;
 import com.hedera.services.legacy.proto.utils.CommonUtils;
 import com.hederahashgraph.api.proto.java.HederaFunctionality;
 import com.hederahashgraph.api.proto.java.Query;
@@ -67,6 +64,7 @@ public class HapiGetTxnRecord extends HapiQueryOp<HapiGetTxnRecord> {
 	boolean useDefaultTxnId = false;
 	boolean requestDuplicates = false;
 	boolean shouldBeTransferFree = false;
+	boolean assertOnlyPriority = false;
 	boolean assertNothingAboutHashes = false;
 	boolean lookupScheduledFromRegistryId = false;
 	Optional<TransactionID> explicitTxnId = Optional.empty();
@@ -102,6 +100,11 @@ public class HapiGetTxnRecord extends HapiQueryOp<HapiGetTxnRecord> {
 		return this;
 	}
 
+	public HapiGetTxnRecord assertingOnlyPriority() {
+		assertOnlyPriority = true;
+		return this;
+	}
+
 	public HapiGetTxnRecord scheduledBy(String creation) {
 		scheduled = true;
 		creationName = Optional.of(creation);
@@ -242,11 +245,11 @@ public class HapiGetTxnRecord extends HapiQueryOp<HapiGetTxnRecord> {
 		if (assertNothing) {
 			return;
 		}
-		if (scheduled) {
-			return;
-		}
 		TransactionRecord actualRecord = response.getTransactionGetRecord().getTransactionRecord();
 		assertPriority(spec, actualRecord);
+		if (scheduled || assertOnlyPriority) {
+			return;
+		}
 		assertDuplicates(spec);
 		if (!assertNothingAboutHashes) {
 			assertTransactionHash(spec, actualRecord);
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java
index 9bfeddeb70..67dde859ba 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/HapiTxnOp.java
@@ -695,6 +695,11 @@ public abstract class HapiTxnOp<T extends HapiTxnOp<T>> extends HapiSpecOperatio
 		return self();
 	}
 
+	public T sansNodeAccount() {
+		omitNodeAccount = true;
+		return self();
+	}
+
 	public TransactionReceipt getLastReceipt() {
 		return lastReceipt;
 	}
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/schedule/HapiScheduleCreate.java b/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/schedule/HapiScheduleCreate.java
index 8c1c042231..5b42d973cb 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/schedule/HapiScheduleCreate.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/spec/transactions/schedule/HapiScheduleCreate.java
@@ -23,12 +23,10 @@ package com.hedera.services.bdd.spec.transactions.schedule;
 import com.google.common.base.MoreObjects;
 import com.google.protobuf.ByteString;
 import com.hedera.services.bdd.spec.HapiApiSpec;
-import com.hedera.services.bdd.spec.HapiPropertySource;
 import com.hedera.services.bdd.spec.HapiSpecSetup;
 import com.hedera.services.bdd.spec.keys.TrieSigMapGenerator;
 import com.hedera.services.bdd.spec.transactions.HapiTxnOp;
 import com.hedera.services.bdd.spec.transactions.TxnUtils;
-import com.hedera.services.legacy.proto.utils.CommonUtils;
 import com.hedera.services.usage.schedule.ScheduleCreateUsage;
 import com.hederahashgraph.api.proto.java.FeeData;
 import com.hederahashgraph.api.proto.java.HederaFunctionality;
@@ -37,7 +35,6 @@ import com.hederahashgraph.api.proto.java.ScheduleCreateTransactionBody;
 import com.hederahashgraph.api.proto.java.SignatureMap;
 import com.hederahashgraph.api.proto.java.Transaction;
 import com.hederahashgraph.api.proto.java.TransactionBody;
-import com.hederahashgraph.api.proto.java.TransactionID;
 import com.hederahashgraph.api.proto.java.TransactionResponse;
 import com.hederahashgraph.fee.SigValueObj;
 import org.apache.logging.log4j.LogManager;
@@ -82,7 +79,7 @@ public class HapiScheduleCreate<T extends HapiTxnOp<T>> extends HapiTxnOp<HapiSc
 
 	public HapiScheduleCreate(String scheduled, HapiTxnOp<T> txn) {
 		this.entity = scheduled;
-		this.scheduled = txn.withLegacyProtoStructure().sansTxnId();
+		this.scheduled = txn.withLegacyProtoStructure().sansTxnId().sansNodeAccount();
 	}
 
 	public HapiScheduleCreate<T> savingExpectedScheduledTxnId() {
diff --git a/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java b/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java
index b5e78bfac5..b49ad1f9d4 100644
--- a/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java
+++ b/test-clients/src/main/java/com/hedera/services/bdd/suites/schedule/ScheduleRecordSpecs.java
@@ -27,11 +27,13 @@ import com.hederahashgraph.api.proto.java.TransactionID;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.atomic.AtomicReference;
 
 import static com.hedera.services.bdd.spec.HapiApiSpec.defaultHapiSpec;
 import static com.hedera.services.bdd.spec.assertions.TransactionRecordAsserts.recordWith;
+import static com.hedera.services.bdd.spec.assertions.TransferListAsserts.exactParticipants;
 import static com.hedera.services.bdd.spec.queries.QueryVerbs.getAccountBalance;
 import static com.hedera.services.bdd.spec.queries.QueryVerbs.getReceipt;
 import static com.hedera.services.bdd.spec.queries.QueryVerbs.getTopicInfo;
@@ -50,6 +52,8 @@ import static com.hedera.services.bdd.spec.utilops.UtilVerbs.sourcing;
 import static com.hedera.services.bdd.spec.utilops.UtilVerbs.usableTxnIdNamed;
 import static com.hedera.services.bdd.spec.utilops.UtilVerbs.validateChargedUsdWithin;
 import static com.hedera.services.bdd.spec.utilops.UtilVerbs.withOpContext;
+import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_PAYER_BALANCE;
+import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.INSUFFICIENT_TX_FEE;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.SUCCESS;
 import static com.hederahashgraph.api.proto.java.ResponseCodeEnum.TRANSACTION_ID_FIELD_NOT_ALLOWED;
 
@@ -75,6 +79,8 @@ public class ScheduleRecordSpecs extends HapiApiSuite {
 						suiteCleanup(),
 						canonicalScheduleOpsHaveExpectedUsdFees(),
 						canScheduleChunkedMessages(),
+						noFeesChargedIfTriggeredPayerIsInsolvent(),
+						noFeesChargedIfTriggeredPayerIsUnwilling(),
 				}
 		);
 	}
@@ -134,6 +140,51 @@ public class ScheduleRecordSpecs extends HapiApiSuite {
 				);
 	}
 
+	public HapiApiSpec noFeesChargedIfTriggeredPayerIsUnwilling() {
+		return defaultHapiSpec("NoFeesChargedIfTriggeredPayerIsUnwilling")
+				.given(
+						cryptoCreate("unwillingPayer")
+				).when(
+						scheduleCreate("schedule",
+								cryptoTransfer(tinyBarsFromTo(GENESIS, FUNDING, 1))
+										.fee(1L)
+										.signedBy(GENESIS, "unwillingPayer")
+						).inheritingScheduledSigs()
+								.via("simpleXferSchedule")
+								.designatingPayer("unwillingPayer")
+								.savingExpectedScheduledTxnId()
+				).then(
+						getTxnRecord("simpleXferSchedule")
+								.scheduledBy("schedule")
+								.hasPriority(recordWith()
+										.transfers(exactParticipants(ignore -> Collections.emptyList()))
+										.status(INSUFFICIENT_TX_FEE))
+
+				);
+	}
+
+	public HapiApiSpec noFeesChargedIfTriggeredPayerIsInsolvent() {
+		return defaultHapiSpec("NoFeesChargedIfTriggeredPayerIsInsolvent")
+				.given(
+						cryptoCreate("insolventPayer").balance(0L)
+				).when(
+						scheduleCreate("schedule",
+								cryptoTransfer(tinyBarsFromTo(GENESIS, FUNDING, 1))
+										.signedBy(GENESIS, "insolventPayer")
+						).inheritingScheduledSigs()
+								.via("simpleXferSchedule")
+								.designatingPayer("insolventPayer")
+								.savingExpectedScheduledTxnId()
+				).then(
+						getTxnRecord("simpleXferSchedule")
+								.scheduledBy("schedule")
+								.hasPriority(recordWith()
+										.transfers(exactParticipants(ignore -> Collections.emptyList()))
+										.status(INSUFFICIENT_PAYER_BALANCE))
+
+				);
+	}
+
 	public HapiApiSpec canScheduleChunkedMessages() {
 		String ofGeneralInterest = "Scotch";
 		AtomicReference<TransactionID> initialTxnId = new AtomicReference<>();
@@ -157,18 +208,39 @@ public class ScheduleRecordSpecs extends HapiApiSuite {
 										.signedBy("payingSender")
 								)
 										.txnId("begin")
+										.logged()
 										.signedBy("payingSender")
 										.inheritingScheduledSigs()
 						),
-						getTxnRecord("begin").scheduled().hasPriority(recordWith().status(SUCCESS))
+						getTxnRecord("begin").hasPriority(recordWith()
+								.status(SUCCESS)
+								.transfers(exactParticipants(spec -> List.of(
+										spec.setup().defaultNode(),
+										spec.setup().fundingAccount(),
+										spec.registry().getAccountID("payingSender")
+								)))).assertingOnlyPriority().logged(),
+						getTxnRecord("begin").scheduled().hasPriority(recordWith()
+								.status(SUCCESS)
+								.transfers(exactParticipants(spec -> List.of(
+										spec.setup().fundingAccount(),
+										spec.registry().getAccountID("payingSender")
+								)))).logged()
 				).then(
 						scheduleCreate("secondChunk",
 								submitMessageTo(ofGeneralInterest)
 										.chunkInfo(3, 2, "payingSender")
 										.signedBy("payingSender")
 						)
+								.via("end")
+								.logged()
 								.payingWith("payingSender")
 								.inheritingScheduledSigs(),
+						getTxnRecord("end").scheduled().hasPriority(recordWith()
+								.status(SUCCESS)
+								.transfers(exactParticipants(spec -> List.of(
+										spec.setup().fundingAccount(),
+										spec.registry().getAccountID("payingSender")
+								)))).logged(),
 						getTopicInfo(ofGeneralInterest).logged().hasSeqNo(2L)
 				);
 	}