diff --git a/tinman/submit.py b/tinman/submit.py
index 3db4eea..94fe525 100755
--- a/tinman/submit.py
+++ b/tinman/submit.py
@@ -19,6 +19,7 @@ from . import util
 
 ACTIONS_MAJOR_VERSION_SUPPORTED = 0
 ACTIONS_MINOR_VERSION_SUPPORTED = 2
+STEEM_BLOCK_INTERVAL = 3
 
 class TransactionSigner(object):
     def __init__(self, sign_transaction_exe=None, chain_id=None):
@@ -168,19 +169,30 @@ def main(argv):
         try:
             if cmd == "metadata":
                 metadata = args
-                transactions_per_block = metadata.get("txgen:transactions_per_block", transactions_per_block)
-                semver = metadata.get("txgen:semver", '0.0')
-                major_version, minor_version = semver.split('.')
-                major_version = int(major_version)
-                minor_version = int(minor_version)
-
-                if major_version == ACTIONS_MAJOR_VERSION_SUPPORTED:
-                    print("metadata:", metadata)
-                else:
-                    raise RuntimeError("Unsupported actions:", metadata)
+                
+                if args.get("post_backfill"):
+                    dgpo = cached_dgpo.get()
+                    now = datetime.datetime.utcnow()
+                    head_block_time = datetime.datetime.strptime(dgpo["time"], "%Y-%m-%dT%H:%M:%S")
+                    join_head = int((now - head_block_time).total_seconds()) // STEEM_BLOCK_INTERVAL
                     
-                if minor_version < ACTIONS_MINOR_VERSION_SUPPORTED:
-                    print("WARNING: Older actions encountered.", file=sys.stderr)
+                    if join_head > STEEM_BLOCK_INTERVAL:
+                        generate_blocks(steemd, {"count": join_head}, cached_dgpo=cached_dgpo, produce_realtime=produce_realtime)
+                        cached_dgpo.reset()
+                else:
+                    transactions_per_block = metadata.get("txgen:transactions_per_block", transactions_per_block)
+                    semver = metadata.get("txgen:semver", '0.0')
+                    major_version, minor_version = semver.split('.')
+                    major_version = int(major_version)
+                    minor_version = int(minor_version)
+
+                    if major_version == ACTIONS_MAJOR_VERSION_SUPPORTED:
+                        print("metadata:", metadata)
+                    else:
+                        raise RuntimeError("Unsupported actions:", metadata)
+                        
+                    if minor_version < ACTIONS_MINOR_VERSION_SUPPORTED:
+                        print("WARNING: Older actions encountered.", file=sys.stderr)
             elif cmd == "wait_blocks":
                 if metadata and args.get("count") == 1 and args.get("miss_blocks"):
                     if args["miss_blocks"] < metadata["recommend:miss_blocks"]:
diff --git a/tinman/txgen.py b/tinman/txgen.py
index 0facf58..04bc774 100755
--- a/tinman/txgen.py
+++ b/tinman/txgen.py
@@ -30,6 +30,7 @@ NUM_BLOCKS_TO_CLEAR_WITNESS_ROUND = 21
 TRANSACTION_WITNESS_SETUP_PAD = 100
 STEEM_MAX_AUTHORITY_MEMBERSHIP = 10
 DENOM = 10**12        # we need stupidly high precision because VESTS
+STEEM_BLOCKS_PER_DAY = 28800
 
 def create_system_accounts(conf, keydb, name):
     desc = conf["accounts"][name]
@@ -431,6 +432,8 @@ def build_actions(conf, silent=True):
         if num_lines > 0:
             metadata["backfill_actions:count"] = num_lines
             metadata["actions:count"] += num_lines
+            miss_blocks -= max(num_lines // transactions_per_block, STEEM_BLOCKS_PER_DAY * 30)
+            metadata["recommend:miss_blocks"] = miss_blocks
             has_backfill = True
     
     yield ["metadata", metadata]
@@ -444,6 +447,8 @@ def build_actions(conf, silent=True):
         with open(backfill_file, "r") as f:
             for line in f:
                 yield json.loads(line)
+        
+        yield ["metadata", {"post_backfill" : True}]
     
     for tx in update_witnesses(conf, keydb, "init"):
         yield ["submit_transaction", {"tx" : tx}]
diff --git a/tinman/util.py b/tinman/util.py
index 605a832..40f7221 100644
--- a/tinman/util.py
+++ b/tinman/util.py
@@ -130,7 +130,7 @@ def action_to_str(action):
     This serializes actions, picking a string that does not occur in the JSON
     serialization to escape public/private key notes.
     """
-    if "esc" in action:
+    if action and action[1] and  "esc" in action[1]:
         return json.dumps(action, separators=(",", ":"), sort_keys=True)
     
     json_empty_esc = json.dumps(action, separators=(",", ":"), default=prockey.PubkeySerializer(esc=""), sort_keys=True)

TN: Submit needs to generate blocks after backfill
Backfill actions start 30 days before realtime.  But if the backfill doesn't supply 30 days of activity, `submit` should just generate blocks, in the situation where the backfill is too short on actions (or if the correct number of actions provided do not actually result in blocks).

To do this, `txgen` should signal that the backfill is done so that `submit` can deal with it or ignore it.  When `txgen` completes the backfill actions, it should signal with metadata.

When `submit` sees the metadata about backfill completion, it will calculate the number of blocks needed to reach realtime.
diff --git a/test/keysub_test.py b/test/keysub_test.py
index d010a19..86d568f 100644
--- a/test/keysub_test.py
+++ b/test/keysub_test.py
@@ -6,7 +6,6 @@ from tinman import keysub
 
 class KeysubTest(unittest.TestCase):
     def test_process_esc(self):
-        # Note, resolver needs to be mocked to properly test.
         self.assertRaises(AttributeError, keysub.process_esc, 'Bpublickey:owner-initminerB', 'B')
     
     def test_process_esc_ignored(self):
@@ -17,7 +16,9 @@ class KeysubTest(unittest.TestCase):
     def test_compute_keypair_from_seed(self):
         try:
             # Try in case the binary is in the path environment.
-            self.assertRaises(json.decoder.JSONDecodeError, keysub.compute_keypair_from_seed, '1234', 'secret')
+            result = keysub.compute_keypair_from_seed('1234', 'secret')
+            expected_result = "('TST6n6jNUngRVCkh3GKBEZVe6r8reBPHmi8bRkwFZ1yh83iKfGcSN', '5JFQtrsidduA79M523UZ2yKub4383BUykWthPkmTD2TAiVfDrA6')"
+            self.assertEqual(result, expected_result)
         except FileNotFoundError:
             # Note, resolver needs to be mocked to properly test.
             true_exe = shutil.which("true")
diff --git a/test/txgen_test.py b/test/txgen_test.py
index ed28747..6f93b8a 100644
--- a/test/txgen_test.py
+++ b/test/txgen_test.py
@@ -223,7 +223,7 @@ class TxgenTest(unittest.TestCase):
                 self.assertEqual(args["txgen:transactions_per_block"], 40)
                 self.assertIsNotNone(args["epoch:created"])
                 self.assertEqual(args["actions:count"], 73)
-                self.assertGreater(args["recommend:miss_blocks"], 28968013)
+                self.assertGreater(args["recommend:miss_blocks"], 28631339)
                 self.assertEqual(args["snapshot:semver"], "0.2")
                 self.assertEqual(args["snapshot:origin_api"], "http://calculon.local")
             elif cmd == "wait_blocks":
@@ -233,9 +233,11 @@ class TxgenTest(unittest.TestCase):
                 self.assertIsInstance(args["tx"]["wif_sigs"], list)
                 for wif in args["tx"]["wif_sigs"]:
                     if isinstance(wif, str):
-                        if len(wif) < 51:
-                            self.assertEqual(args["esc"], wif[0])
-                            self.assertEqual(args["esc"], wif[-1])
+                        esc = args.get("esc", None)
+                        
+                        if esc and len(wif) < 51:
+                            self.assertEqual(esc, wif[0])
+                            self.assertEqual(esc, wif[-1])
                         else:
                             self.assertEqual(len(wif), 51)
                     else:
diff --git a/test/util_test.py b/test/util_test.py
index 5ecd3d8..e93481e 100644
--- a/test/util_test.py
+++ b/test/util_test.py
@@ -69,3 +69,8 @@ class UtilTest(unittest.TestCase):
         action = ["metadata", {}]
         result = util.action_to_str(action)
         self.assertEqual(result, '["metadata",{"esc":"b"}]')
+
+    def test_action_to_str_with_esc(self):
+        action = ["metadata", {"esc": "C"}]
+        result = util.action_to_str(action)
+        self.assertEqual(result, '["metadata",{"esc":"C"}]')

pip install -e .
parse_log_pytest
Pipfile
ijson pytest
apt-get update apt-get install -y libyajl-dev
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs==22.2.0
certifi==2021.5.30
ijson==3.3.0
importlib-metadata==4.8.3
iniconfig==1.1.1
packaging==21.3
pipfile==0.0.2
pluggy==1.0.0
py==1.11.0
pyparsing==3.1.4
pytest==7.0.1
-e git+https://github.com/steemit/tinman.git@910179ae21242ab567bbd52e2d4070dd3c3c4b40#egg=tinman
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
typing_extensions==4.1.1
zipp==3.6.0

name: tinman
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - pip=21.2.2=py36h06a4308_0
  - pipfile=0.0.2=py_0
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - attrs==22.2.0
      - ijson==3.3.0
      - importlib-metadata==4.8.3
      - iniconfig==1.1.1
      - packaging==21.3
      - pluggy==1.0.0
      - py==1.11.0
      - pyparsing==3.1.4
      - pytest==7.0.1
      - tomli==1.2.3
      - typing-extensions==4.1.1
      - zipp==3.6.0
prefix: /opt/conda/envs/tinman

test/util_test.py::UtilTest::test_action_to_str_with_esc
test/txgen_test.py::TxgenTest::test_build_actions test/txgen_test.py::TxgenTest::test_build_actions_future_snapshot test/txgen_test.py::TxgenTest::test_create_accounts test/txgen_test.py::TxgenTest::test_get_account_stats test/txgen_test.py::TxgenTest::test_get_proportions test/txgen_test.py::TxgenTest::test_update_accounts
test/keysub_test.py::KeysubTest::test_compute_keypair_from_seed test/keysub_test.py::KeysubTest::test_process_esc test/keysub_test.py::KeysubTest::test_process_esc_ignored test/txgen_test.py::TxgenTest::test_create_system_accounts_bad_args test/txgen_test.py::TxgenTest::test_create_witnesses test/txgen_test.py::TxgenTest::test_update_witnesses test/txgen_test.py::TxgenTest::test_vote_witnesses test/util_test.py::UtilTest::test_action_to_str test/util_test.py::UtilTest::test_batch test/util_test.py::UtilTest::test_find_non_substr test/util_test.py::UtilTest::test_iterate_operations_from test/util_test.py::UtilTest::test_tag_escape_sequences
tinman/txgen.py tinman/util.py tinman/submit.py
tinman/txgen.py tinman/util.py tinman/submit.py