diff --git a/src/validation.cpp b/src/validation.cpp
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -420,6 +420,11 @@
     return IsMagneticAnomalyEnabled(config, chainActive.Tip());
 }
 
+static bool IsGravitonEnabledForCurrentBlock(const Config &config) {
+    AssertLockHeld(cs_main);
+    return IsGravitonEnabled(config, chainActive.Tip());
+}
+
 // Command-line argument "-replayprotectionactivationtime=<timestamp>" will
 // cause the node to switch to replay protected SigHash ForkID value when the
 // median timestamp of the previous 11 blocks is greater than or equal to
@@ -744,6 +749,10 @@
             extraFlags |= SCRIPT_VERIFY_CHECKDATASIG_SIGOPS;
         }
 
+        if (IsGravitonEnabledForCurrentBlock(config)) {
+            extraFlags |= SCRIPT_ENABLE_SCHNORR_MULTISIG;
+        }
+
         // Make sure whatever we need to activate is actually activated.
         const uint32_t scriptVerifyFlags =
             STANDARD_SCRIPT_VERIFY_FLAGS | extraFlags;
@@ -1264,6 +1273,25 @@
                 }
             }
 
+            // Before banning, we need to check whether the transaction would
+            // be valid on the other side of the upgrade, so as to avoid
+            // splitting the network between upgraded and non-upgraded nodes.
+            // Note that this will create strange error messages like
+            // "upgrade-conditional-script-failure (Dummy CHECKMULTISIG argument
+            // must be zero)" -- the tx was initially refused entry due to
+            // NULLDUMMY, a standardness flag, but it is outright invalid before
+            // the upgrade as it contains schnorr signatures; it would however
+            // be valid after the upgrade.
+            CScriptCheck check3(scriptPubKey, amount, tx, i,
+                                mandatoryFlags ^ SCRIPT_ENABLE_SCHNORR_MULTISIG,
+                                sigCacheStore, txdata);
+            if (check3()) {
+                return state.Invalid(
+                    false, REJECT_INVALID,
+                    strprintf("upgrade-conditional-script-failure (%s)",
+                              ScriptErrorString(check.GetScriptError())));
+            }
+
             // Failures of other flags indicate a transaction that is invalid in
             // new blocks, e.g. a invalid P2SH. We DoS ban such nodes as they
             // are not following the protocol. That said during an upgrade
@@ -1599,6 +1627,10 @@
         flags |= SCRIPT_VERIFY_CLEANSTACK;
     }
 
+    if (IsGravitonEnabled(config, pindex)) {
+        flags |= SCRIPT_ENABLE_SCHNORR_MULTISIG;
+    }
+
     // We make sure this node will have replay protection during the next hard
     // fork.
     if (IsReplayProtectionEnabled(config, pindex)) {
diff --git a/test/functional/abc-schnorrmultisig-activation.py b/test/functional/abc-schnorrmultisig-activation.py
new file mode 100755
--- /dev/null
+++ b/test/functional/abc-schnorrmultisig-activation.py
@@ -0,0 +1,393 @@
+#!/usr/bin/env python3
+# Copyright (c) 2019 The Bitcoin developers
+# Distributed under the MIT software license, see the accompanying
+# file COPYING or http://www.opensource.org/licenses/mit-license.php.
+"""
+This tests the activation of the upgraded CHECKMULTISIG mode that uses
+Schnorr transaction signatures and repurposes the dummy element to indicate
+which signatures are being checked.
+- acceptance both in mempool and blocks.
+- check non-banning for peers who send invalid txns that would have been valid
+on the other side of the upgrade.
+- check banning of peers for some fully-invalid transactions.
+
+Derived from abc-schnorr.py
+"""
+
+from test_framework.blocktools import (
+    create_block,
+    create_coinbase,
+    create_transaction,
+    make_conform_to_ctor,
+)
+from test_framework.key import CECKey
+from test_framework.messages import (
+    CBlock,
+    COutPoint,
+    CTransaction,
+    CTxIn,
+    CTxOut,
+    FromHex,
+    ToHex,
+)
+from test_framework.mininode import (
+    network_thread_join,
+    network_thread_start,
+    P2PDataStore,
+)
+from test_framework import schnorr
+from test_framework.script import (
+    CScript,
+    OP_0,
+    OP_1,
+    OP_CHECKMULTISIG,
+    OP_TRUE,
+    SIGHASH_ALL,
+    SIGHASH_FORKID,
+    SignatureHashForkId,
+)
+from test_framework.test_framework import BitcoinTestFramework
+from test_framework.util import assert_equal, assert_raises_rpc_error
+
+# the upgrade activation time, which we artificially set far into the future
+GRAVITON_START_TIME = 2000000000
+
+# If we don't do this, autoreplay protection will activate before graviton and
+# all our sigs will mysteriously fail.
+REPLAY_PROTECTION_START_TIME = GRAVITON_START_TIME * 2
+
+
+# Before the upgrade, Schnorr checkmultisig is rejected but forgiven if it would have been valid after the upgrade.
+PREUPGRADE_SCHNORR_MULTISIG_ERROR = dict(reject_code=16,
+                                         reject_reason=b'upgrade-conditional-script-failure (Dummy CHECKMULTISIG argument must be zero)')
+
+# Before the upgrade, ECDSA checkmultisig with non-null dummy are rejected with a non-mandatory error.
+PREUPGRADE_ECDSA_NULLDUMMY_ERROR = dict(reject_code=64,
+                                        reject_reason=b'non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero)')
+# After the upgrade, ECDSA checkmultisig with non-null dummy are invalid since the new mode refuses ECDSA, but still do not result in ban.
+POSTUPGRADE_ECDSA_NULLDUMMY_ERROR = dict(reject_code=16,
+                                         reject_reason=b'upgrade-conditional-script-failure (Only Schnorr signatures allowed in this operation)')
+
+# A mandatory (bannable) error occurs when people pass Schnorr signatures into legacy OP_CHECKMULTISIG; this is the case on both sides of the upgrade.
+SCHNORR_LEGACY_MULTISIG_ERROR = dict(reject_code=16,
+                                     reject_reason=b'mandatory-script-verify-flag-failed (Signature cannot be 65 bytes in CHECKMULTISIG)')
+
+# Blocks with invalid scripts give this error:
+BADINPUTS_ERROR = dict(reject_code=16,
+                       reject_reason=b'blk-bad-inputs')
+
+
+def rpc_error(*, reject_code, reject_reason):
+    # RPC indicates rejected items in a slightly different way than p2p.
+    return '{:s} (code {:d})'.format(reject_reason.decode(), reject_code)
+
+
+# This 64-byte signature is used to test exclusion & banning according to
+# the above error messages.
+# Tests of real 64 byte ECDSA signatures can be found in script_tests.
+sig64 = b'\0'*64
+
+
+class SchnorrTest(BitcoinTestFramework):
+
+    def set_test_params(self):
+        self.num_nodes = 1
+        self.block_heights = {}
+        self.extra_args = [["-gravitonactivationtime={}".format(
+            GRAVITON_START_TIME),
+            "-replayprotectionactivationtime={}".format(
+            REPLAY_PROTECTION_START_TIME)]]
+
+    def bootstrap_p2p(self, *, num_connections=1):
+        """Add a P2P connection to the node.
+
+        Helper to connect and wait for version handshake."""
+        for _ in range(num_connections):
+            self.nodes[0].add_p2p_connection(P2PDataStore())
+        network_thread_start()
+        self.nodes[0].p2p.wait_for_verack()
+
+    def reconnect_p2p(self, **kwargs):
+        """Tear down and bootstrap the P2P connection to the node.
+
+        The node gets disconnected several times in this test. This helper
+        method reconnects the p2p and restarts the network thread."""
+        self.nodes[0].disconnect_p2ps()
+        network_thread_join()
+        self.bootstrap_p2p(**kwargs)
+
+    def getbestblock(self, node):
+        """Get the best block. Register its height so we can use build_block."""
+        block_height = node.getblockcount()
+        blockhash = node.getblockhash(block_height)
+        block = FromHex(CBlock(), node.getblock(blockhash, 0))
+        block.calc_sha256()
+        self.block_heights[block.sha256] = block_height
+        return block
+
+    def build_block(self, parent, transactions=(), nTime=None):
+        """Make a new block with an OP_1 coinbase output.
+
+        Requires parent to have its height registered."""
+        parent.calc_sha256()
+        block_height = self.block_heights[parent.sha256] + 1
+        block_time = (parent.nTime + 1) if nTime is None else nTime
+
+        block = create_block(
+            parent.sha256, create_coinbase(block_height), block_time)
+        block.vtx.extend(transactions)
+        make_conform_to_ctor(block)
+        block.hashMerkleRoot = block.calc_merkle_root()
+        block.solve()
+        self.block_heights[block.sha256] = block_height
+        return block
+
+    def check_for_ban_on_rejected_tx(self, tx, reject_code=None, reject_reason=None):
+        """Check we are disconnected when sending a txn that the node rejects.
+
+        (Can't actually get banned, since bitcoind won't ban local peers.)"""
+        self.nodes[0].p2p.send_txs_and_test(
+            [tx], self.nodes[0], success=False, expect_disconnect=True, reject_code=reject_code, reject_reason=reject_reason)
+        self.reconnect_p2p()
+
+    def check_for_no_ban_on_rejected_tx(self, tx, reject_code, reject_reason):
+        """Check we are not disconnected when sending a txn that the node rejects."""
+        self.nodes[0].p2p.send_txs_and_test(
+            [tx], self.nodes[0], success=False, reject_code=reject_code, reject_reason=reject_reason)
+
+    def check_for_ban_on_rejected_block(self, block, reject_code=None, reject_reason=None):
+        """Check we are disconnected when sending a block that the node rejects.
+
+        (Can't actually get banned, since bitcoind won't ban local peers.)"""
+        self.nodes[0].p2p.send_blocks_and_test(
+            [block], self.nodes[0], success=False, reject_code=reject_code, reject_reason=reject_reason)
+        self.nodes[0].p2p.wait_for_disconnect()
+        self.reconnect_p2p()
+
+    def run_test(self):
+        node, = self.nodes
+
+        self.bootstrap_p2p()
+
+        tip = self.getbestblock(node)
+
+        self.log.info("Create some blocks with OP_1 coinbase for spending.")
+        blocks = []
+        for _ in range(10):
+            tip = self.build_block(tip)
+            blocks.append(tip)
+        node.p2p.send_blocks_and_test(blocks, node, success=True)
+        spendable_outputs = [block.vtx[0] for block in blocks]
+
+        self.log.info("Mature the blocks and get out of IBD.")
+        node.generate(100)
+
+        tip = self.getbestblock(node)
+
+        self.log.info("Setting up spends to test and mining the fundings.")
+        fundings = []
+
+        # Generate a key pair
+        privkeybytes = b"Schnorr!" * 4
+        private_key = CECKey()
+        private_key.set_secretbytes(privkeybytes)
+        # get uncompressed public key serialization
+        public_key = private_key.get_pubkey()
+
+        def create_fund_and_spend_tx(dummy=OP_0, sigtype='ecdsa'):
+            spendfrom = spendable_outputs.pop()
+
+            script = CScript([OP_1, public_key, OP_1, OP_CHECKMULTISIG])
+
+            value = spendfrom.vout[0].nValue
+
+            # Fund transaction
+            txfund = create_transaction(spendfrom, 0, b'', value, script)
+            txfund.rehash()
+            fundings.append(txfund)
+
+            # Spend transaction
+            txspend = CTransaction()
+            txspend.vout.append(
+                CTxOut(value-1000, CScript([OP_TRUE])))
+            txspend.vin.append(
+                CTxIn(COutPoint(txfund.sha256, 0), b''))
+
+            # Sign the transaction
+            sighashtype = SIGHASH_ALL | SIGHASH_FORKID
+            hashbyte = bytes([sighashtype & 0xff])
+            sighash = SignatureHashForkId(
+                script, txspend, 0, sighashtype, value)
+            if sigtype == 'schnorr':
+                txsig = schnorr.sign(privkeybytes, sighash) + hashbyte
+            elif sigtype == 'ecdsa':
+                txsig = private_key.sign(sighash) + hashbyte
+            txspend.vin[0].scriptSig = CScript([dummy, txsig])
+            txspend.rehash()
+
+            return txspend
+
+        # two of these transactions, which are valid both before and after upgrade.
+        ecdsa0tx = create_fund_and_spend_tx(OP_0, 'ecdsa')
+        ecdsa0tx_2 = create_fund_and_spend_tx(OP_0, 'ecdsa')
+
+        # two of these, which are nonstandard before upgrade and invalid after.
+        ecdsa1tx = create_fund_and_spend_tx(OP_1, 'ecdsa')
+        ecdsa1tx_2 = create_fund_and_spend_tx(OP_1, 'ecdsa')
+
+        # this one is always invalid.
+        schnorr0tx = create_fund_and_spend_tx(OP_0, 'schnorr')
+
+        # this one is only going to be valid after the upgrade.
+        schnorr1tx = create_fund_and_spend_tx(OP_1, 'schnorr')
+
+        tip = self.build_block(tip, fundings)
+        node.p2p.send_blocks_and_test([tip], node)
+
+        self.log.info("Start preupgrade tests")
+
+        self.log.info("Sending rejected transactions via RPC")
+        assert_raises_rpc_error(-26, rpc_error(**PREUPGRADE_ECDSA_NULLDUMMY_ERROR),
+                                node.sendrawtransaction, ToHex(ecdsa1tx))
+        assert_raises_rpc_error(-26, rpc_error(**SCHNORR_LEGACY_MULTISIG_ERROR),
+                                node.sendrawtransaction, ToHex(schnorr0tx))
+        assert_raises_rpc_error(-26, rpc_error(**PREUPGRADE_SCHNORR_MULTISIG_ERROR),
+                                node.sendrawtransaction, ToHex(schnorr1tx))
+
+        self.log.info(
+            "Sending rejected transactions via net (banning depending on situation)")
+        self.check_for_no_ban_on_rejected_tx(
+            ecdsa1tx, **PREUPGRADE_ECDSA_NULLDUMMY_ERROR)
+        self.check_for_ban_on_rejected_tx(
+            schnorr0tx, **SCHNORR_LEGACY_MULTISIG_ERROR)
+        self.check_for_no_ban_on_rejected_tx(
+            schnorr1tx, **PREUPGRADE_SCHNORR_MULTISIG_ERROR)
+
+        self.log.info(
+            "Sending invalid transactions in blocks (and get banned!)")
+        self.check_for_ban_on_rejected_block(
+            self.build_block(tip, [schnorr0tx]), **BADINPUTS_ERROR)
+        self.check_for_ban_on_rejected_block(
+            self.build_block(tip, [schnorr1tx]), **BADINPUTS_ERROR)
+
+        self.log.info("Sending valid transaction via net, then mining it")
+        node.p2p.send_txs_and_test([ecdsa0tx], node)
+        assert_equal(node.getrawmempool(), [ecdsa0tx.hash])
+        tip = self.build_block(tip, [ecdsa0tx])
+        node.p2p.send_blocks_and_test([tip], node)
+        assert_equal(node.getrawmempool(), [])
+
+        # Activation tests
+
+        self.log.info("Approach to just before upgrade activation")
+        # Move our clock to the uprade time so we will accept such future-timestamped blocks.
+        node.setmocktime(GRAVITON_START_TIME)
+        # Mine six blocks with timestamp starting at GRAVITON_START_TIME-1
+        blocks = []
+        for i in range(-1, 5):
+            tip = self.build_block(tip, nTime=GRAVITON_START_TIME + i)
+            blocks.append(tip)
+        node.p2p.send_blocks_and_test(blocks, node)
+        assert_equal(node.getblockchaininfo()[
+                     'mediantime'], GRAVITON_START_TIME - 1)
+
+        self.log.info(
+            "The next block will activate, but the activation block itself must follow old rules")
+        self.check_for_ban_on_rejected_block(
+            self.build_block(tip, [schnorr0tx]), **BADINPUTS_ERROR)
+
+        self.log.info(
+            "Send a lecacy ECDSA multisig into mempool, we will check after upgrade to make sure it didn't get cleaned out unnecessarily.")
+        node.p2p.send_txs_and_test([ecdsa0tx_2], node)
+        assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash])
+
+        # save this tip for later
+        preupgrade_block = tip
+
+        self.log.info(
+            "Mine the activation block itself, including a legacy nulldummy violation at the last possible moment")
+        tip = self.build_block(tip, [ecdsa1tx])
+        node.p2p.send_blocks_and_test([tip], node)
+
+        self.log.info("We have activated!")
+        assert_equal(node.getblockchaininfo()[
+                     'mediantime'], GRAVITON_START_TIME)
+        assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash])
+
+        # save this tip for later
+        upgrade_block = tip
+
+        self.log.info(
+            "Trying to mine a legacy nulldummy violation, but we are just barely too late")
+        self.check_for_ban_on_rejected_block(
+            self.build_block(tip, [ecdsa1tx_2]), **BADINPUTS_ERROR)
+        self.log.info(
+            "If we try to submit it by mempool or RPC, the error code has changed but we still aren't banned")
+        assert_raises_rpc_error(-26, rpc_error(**POSTUPGRADE_ECDSA_NULLDUMMY_ERROR),
+                                node.sendrawtransaction, ToHex(ecdsa1tx_2))
+        self.check_for_no_ban_on_rejected_tx(
+            ecdsa1tx_2, **POSTUPGRADE_ECDSA_NULLDUMMY_ERROR)
+
+        self.log.info(
+            "Submitting a new Schnorr-multisig via net, and mining it in a block")
+        node.p2p.send_txs_and_test([schnorr1tx], node)
+        assert_equal(set(node.getrawmempool()), {
+                     ecdsa0tx_2.hash, schnorr1tx.hash})
+        tip = self.build_block(tip, [schnorr1tx])
+        node.p2p.send_blocks_and_test([tip], node)
+
+        # save this tip for later
+        postupgrade_block = tip
+
+        self.log.info(
+            "That legacy ECDSA multisig is still in mempool, let's mine it")
+        assert_equal(node.getrawmempool(), [ecdsa0tx_2.hash])
+        tip = self.build_block(tip, [ecdsa0tx_2])
+        node.p2p.send_blocks_and_test([tip], node)
+        assert_equal(node.getrawmempool(), [])
+
+        self.log.info(
+            "Trying Schnorr in legacy multisig remains invalid and banworthy as ever")
+        self.check_for_ban_on_rejected_tx(
+            schnorr0tx, **SCHNORR_LEGACY_MULTISIG_ERROR)
+        self.check_for_ban_on_rejected_block(
+            self.build_block(tip, [schnorr0tx]), **BADINPUTS_ERROR)
+
+        # Deactivation tests
+
+        self.log.info(
+            "Invalidating the post-upgrade blocks returns the transactions to mempool")
+        node.invalidateblock(postupgrade_block.hash)
+        assert_equal(set(node.getrawmempool()), {
+                     ecdsa0tx_2.hash, schnorr1tx.hash})
+        self.log.info(
+            "Invalidating the upgrade block evicts the transactions valid only after upgrade")
+        node.invalidateblock(upgrade_block.hash)
+        assert_equal(set(node.getrawmempool()), {ecdsa0tx_2.hash})
+
+        self.log.info("Return to our tip")
+        node.reconsiderblock(upgrade_block.hash)
+        node.reconsiderblock(postupgrade_block.hash)
+        assert_equal(node.getbestblockhash(), tip.hash)
+        assert_equal(node.getrawmempool(), [])
+
+        self.log.info(
+            "Create an empty-block reorg that forks from pre-upgrade")
+        tip = preupgrade_block
+        blocks = []
+        for _ in range(10):
+            tip = self.build_block(tip)
+            blocks.append(tip)
+        node.p2p.send_blocks_and_test(blocks, node)
+
+        self.log.info("Transactions from orphaned blocks are sent into mempool ready to be mined again, including upgrade-dependent ones even though the fork deactivated and reactivated the upgrade.")
+        assert_equal(set(node.getrawmempool()), {
+                     ecdsa0tx_2.hash, schnorr1tx.hash})
+        node.generate(1)
+        tip = self.getbestblock(node)
+        assert set(tx.rehash() for tx in tip.vtx).issuperset(
+            {ecdsa0tx_2.hash, schnorr1tx.hash})
+
+
+if __name__ == '__main__':
+    SchnorrTest().main()
diff --git a/test/functional/feature_nulldummy.py b/test/functional/feature_nulldummy.py
--- a/test/functional/feature_nulldummy.py
+++ b/test/functional/feature_nulldummy.py
@@ -22,6 +22,12 @@
 from test_framework.test_framework import BitcoinTestFramework
 from test_framework.util import assert_equal, assert_raises_rpc_error
 
+# This test checks for a reject reason that changes after the graviton
+# upgrade. Since the nulldummy effect and this test are destined to be removed
+# after the upgrade anyway, we run this test pre-upgrade only.
+# More detailed dummy tests can be found in abc-schnorrmultisig-activation.py.
+GRAVITON_START_TIME = 2000000000
+
 NULLDUMMY_ERROR = "non-mandatory-script-verify-flag (Dummy CHECKMULTISIG argument must be zero) (code 64)"
 
 
@@ -43,7 +49,8 @@
     def set_test_params(self):
         self.num_nodes = 1
         self.setup_clean_chain = True
-        self.extra_args = [['-whitelist=127.0.0.1']]
+        self.extra_args = [['-whitelist=127.0.0.1',
+                            "-gravitonactivationtime={}".format(GRAVITON_START_TIME)]]
 
     def run_test(self):
         self.address = self.nodes[0].getnewaddress()