diff --git a/.gitignore b/.gitignore
index 0ff94a4eed18d00308f16413d4c621c830259006..75fb4a5164c45d3cd814da69c3eb0ebf3a27efd3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,9 +58,9 @@
 /src/knot/conf/libknotd_la-cf-lex.c
 /src/knot/conf/libknotd_la-cf-parse.c
 /src/knot/conf/libknotd_la-cf-parse.h
-/src/zscanner/scanner.c
 
 # zscanner
+/src/zscanner/scanner.c
 /src/zscanner/tests/tmp/
 /src/zscanner/tests/unittests
 /src/zscanner/tests/zscanner-tool
@@ -74,7 +74,7 @@
 /src/knsec3hash
 *.gcda
 *.gcno
-/*-coverage.info
+/*.info
 /*coverage/
 
 # alternative allocators
diff --git a/Knot.files b/Knot.files
index 66d882fb000232b9068d4f214098be48a010093e..497a6727ef64d86486cff6d2262c150ac97e36d8 100644
--- a/Knot.files
+++ b/Knot.files
@@ -157,8 +157,8 @@ src/knot/updates/changesets.c
 src/knot/updates/changesets.h
 src/knot/updates/ddns.c
 src/knot/updates/ddns.h
-src/knot/updates/xfr-in.c
-src/knot/updates/xfr-in.h
+src/knot/updates/apply.c
+src/knot/updates/apply.h
 src/knot/worker/pool.c
 src/knot/worker/pool.h
 src/knot/worker/queue.c
diff --git a/Makefile.am b/Makefile.am
index 4885799b9aee1c38a7963c5fd68e5ab5bbcd4d0a..33f9e2beeaff20a1e4be2198b6408afd2fade645 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,16 +1,50 @@
 ACLOCAL_AMFLAGS = -I m4
 SUBDIRS = libtap src tests samples doc man patches
 
-AM_DISTCHECK_CONFIGURE_FLAGS =		\
+AM_DISTCHECK_CONFIGURE_FLAGS = \
 	--disable-code-coverage
 
 code_coverage_quiet = --quiet
 
 check-code-coverage:
 if CODE_COVERAGE_ENABLED
+	$(MAKE) $(AM_MAKEFLAGS) code-coverage-initial
 	-$(MAKE) $(AM_MAKEFLAGS) -k check
 	$(MAKE) $(AM_MAKEFLAGS) code-coverage-capture
 	$(MAKE) $(AM_MAKEFLAGS) code-coverage-html
+	$(MAKE) $(AM_MAKEFLAGS) code-coverage-summary
+else
+	@echo "You need to run configure with --enable-code-coverage to enable code coverage"
+endif
+
+code-coverage-initial:
+if CODE_COVERAGE_ENABLED
+	$(LCOV) $(code_coverage_quiet) \
+		--directory $(top_builddir)/src/common \
+		--directory $(top_builddir)/src/knot \
+		--directory $(top_builddir)/src/libknot \
+		--capture --initial \
+		--ignore-errors source \
+		--base-directory $(top_builddir)/src \
+		--output-file "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-src_base.info" \
+		--test-name "$(PACKAGE_NAME)-$(PACKAGE_VERSION)" \
+		--no-checksum --compat-libtool
+	$(LCOV) $(code_coverage_quiet) \
+		--directory $(top_builddir)/src/knot/conf \
+		--capture --initial \
+		--ignore-errors source \
+		--base-directory $(top_builddir)/src/knot/conf \
+		--output-file "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-conf_base.info" \
+		--test-name "$(PACKAGE_NAME)-$(PACKAGE_VERSION)" \
+		--no-checksum --compat-libtool
+	$(LCOV) $(code_coverage_quiet) \
+		--directory $(top_builddir)/src/zscanner \
+		--capture --initial \
+		--ignore-errors source \
+		--base-directory $(top_builddir)/src/zscanner \
+		--output-file "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-zscanner_base.info" \
+		--test-name "$(PACKAGE_NAME)-$(PACKAGE_VERSION)" \
+		--no-checksum --compat-libtool
 else
 	@echo "You need to run configure with --enable-code-coverage to enable code coverage"
 endif
@@ -44,11 +78,13 @@ if CODE_COVERAGE_ENABLED
 		--test-name "$(PACKAGE_NAME)-$(PACKAGE_VERSION)" \
 		--no-checksum --compat-libtool
 	$(LCOV) $(code_coverage_quiet) \
+		--add-tracefile "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-src_base.info" \
+		--add-tracefile "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-conf_base.info" \
+		--add-tracefile "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-zscanner_base.info" \
 		--add-tracefile "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-src_capture.info" \
 		--add-tracefile "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-conf_capture.info" \
 		--add-tracefile "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-zscanner_capture.info" \
 		--output-file "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage.info"
-	-@rm -f $(PACKAGE_NAME)-$(PACKAGE_VERSION)-*_base.info $(PACKAGE_NAME)-$(PACKAGE_VERSION)-*_capture.info
 else
 	@echo "You need to run configure with --enable-code-coverage to enable code coverage"
 endif
@@ -63,14 +99,24 @@ else
 	@echo "You need to run configure with --enable-code-coverage to enable code coverage"
 endif
 
+code-coverage-summary:
+if CODE_COVERAGE_ENABLED
+	$(LCOV) \
+		--summary "$(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage.info"
+else
+	@echo "You need to run configure with --enable-code-coverage to enable code coverage"
+endif
+
 if CODE_COVERAGE_ENABLED
 clean-local: code-coverage-clean
+	-find . -name "*.gcno" -delete
 code-coverage-clean:
 	-$(LCOV) --directory $(top_builddir) -z
 	-rm -rf $(PACKAGE_NAME)-$(PACKAGE_VERSION)-*_base.info \
-	        $(PACKAGE_NAME)-$(PACKAGE_VERSION)-*_coverage.info \
+	        $(PACKAGE_NAME)-$(PACKAGE_VERSION)-*_capture.info \
+	        $(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage.info \
 	        $(PACKAGE_NAME)-$(PACKAGE_VERSION)-coverage/
 	-find . -name "*.gcda" -o -name "*.gcov" -delete
 endif
 
-.PHONY: check-code-coverage code-coverage-initial code-coverage-capture code-coverage-html code-coverage-clean
+.PHONY: check-code-coverage code-coverage-initial code-coverage-capture code-coverage-html code-coverage-summary code-coverage-clean
diff --git a/src/Makefile.am b/src/Makefile.am
index e61a2e18048f9fb18bd873a57d4288d04afec34a..f0974df31227b2352fce172d2c0c38dad339c0bd 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -273,8 +273,8 @@ libknotd_la_SOURCES =				\
 	knot/updates/changesets.h		\
 	knot/updates/ddns.c			\
 	knot/updates/ddns.h			\
-	knot/updates/xfr-in.c			\
-	knot/updates/xfr-in.h			\
+	knot/updates/apply.c			\
+	knot/updates/apply.h			\
 	knot/worker/pool.c			\
 	knot/worker/pool.h			\
 	knot/worker/queue.c			\
diff --git a/src/knot/dnssec/nsec-chain.c b/src/knot/dnssec/nsec-chain.c
index 07acd248b889aff9c014d61ab95d76ca930fa8ca..1ca9d58ff923092c78b678ef797e1782ad1ff8d7 100644
--- a/src/knot/dnssec/nsec-chain.c
+++ b/src/knot/dnssec/nsec-chain.c
@@ -142,8 +142,8 @@ static int connect_nsec_nodes(zone_node_t *a, zone_node_t *b,
 
 	dbg_dnssec_detail("Adding new NSEC to changeset.\n");
 	// Add new NSEC to the changeset (no matter if old was removed)
-	return knot_changeset_add_rrset(data->changeset, new_nsec,
-	                                KNOT_CHANGESET_ADD);
+	return changeset_add_rrset(data->changeset, new_nsec,
+	                                CHANGESET_ADD);
 }
 
 /* - API - iterations ------------------------------------------------------- */
@@ -205,7 +205,7 @@ int knot_nsec_chain_iterate_create(knot_zone_tree_t *nodes,
  * \brief Add entry for removed NSEC to the changeset.
  */
 int knot_nsec_changeset_remove(const zone_node_t *n,
-                               knot_changeset_t *changeset)
+                               changeset_t *changeset)
 {
 	if (changeset == NULL) {
 		return KNOT_EINVAL;
@@ -219,8 +219,8 @@ int knot_nsec_changeset_remove(const zone_node_t *n,
 	}
 	if (nsec) {
 		// update changeset
-		result = knot_changeset_add_rrset(changeset, nsec,
-		                                  KNOT_CHANGESET_REMOVE);
+		result = changeset_add_rrset(changeset, nsec,
+		                                  CHANGESET_REMOVE);
 		if (result != KNOT_EOK) {
 			knot_rrset_free(&nsec, NULL);
 			return result;
@@ -253,8 +253,8 @@ int knot_nsec_changeset_remove(const zone_node_t *n,
 		}
 
 		// store RRSIG
-		result = knot_changeset_add_rrset(changeset, synth_rrsigs,
-		                                  KNOT_CHANGESET_REMOVE);
+		result = changeset_add_rrset(changeset, synth_rrsigs,
+		                                  CHANGESET_REMOVE);
 		if (result != KNOT_EOK) {
 			knot_rrset_free(&synth_rrsigs, NULL);
 			return result;
@@ -288,7 +288,7 @@ bool knot_nsec_empty_nsec_and_rrsigs_in_node(const zone_node_t *n)
  * \brief Create new NSEC chain, add differences from current into a changeset.
  */
 int knot_nsec_create_chain(const zone_contents_t *zone, uint32_t ttl,
-                           knot_changeset_t *changeset)
+                           changeset_t *changeset)
 {
 	assert(zone);
 	assert(zone->nodes);
diff --git a/src/knot/dnssec/nsec-chain.h b/src/knot/dnssec/nsec-chain.h
index 0a9c17804d5bf66c2846e3d870df3072dd91f2ac..db0000220b7a674ac3a45981eafeda6b903b2703 100644
--- a/src/knot/dnssec/nsec-chain.h
+++ b/src/knot/dnssec/nsec-chain.h
@@ -39,7 +39,7 @@
  */
 typedef struct {
 	uint32_t ttl;                      // TTL for NSEC(3) records
-	knot_changeset_t *changeset;       // Changeset for NSEC(3) changes
+	changeset_t *changeset;       // Changeset for NSEC(3) changes
 	const zone_contents_t *zone;  // Updated zone
 } nsec_chain_iterate_data_t;
 
@@ -96,7 +96,7 @@ int knot_nsec_chain_iterate_create(knot_zone_tree_t *nodes,
  * \return Error code, KNOT_EOK if successful.
  */
 int knot_nsec_changeset_remove(const zone_node_t *n,
-                               knot_changeset_t *changeset);
+                               changeset_t *changeset);
 
 /*!
  * \brief Checks whether the node is empty or eventually contains only NSEC and
@@ -119,6 +119,6 @@ bool knot_nsec_empty_nsec_and_rrsigs_in_node(const zone_node_t *n);
  * \return Error code, KNOT_EOK if successful.
  */
 int knot_nsec_create_chain(const zone_contents_t *zone, uint32_t ttl,
-                           knot_changeset_t *changeset);
+                           changeset_t *changeset);
 
 #endif // _KNOT_DNSSEC_NSEC_CHAIN_FIX_H_
diff --git a/src/knot/dnssec/nsec3-chain.c b/src/knot/dnssec/nsec3-chain.c
index 1495e48cf80f4006a1bca2c5e4379f4f9444488c..a64726385efb3837572abe5b7863ef7015214c0e 100644
--- a/src/knot/dnssec/nsec3-chain.c
+++ b/src/knot/dnssec/nsec3-chain.c
@@ -415,7 +415,7 @@ static int connect_nsec3_nodes(zone_node_t *a, zone_node_t *b,
  */
 static int create_nsec3_nodes(const zone_contents_t *zone, uint32_t ttl,
                               knot_zone_tree_t *nsec3_nodes,
-                              knot_changeset_t *chgset)
+                              changeset_t *chgset)
 {
 	assert(zone);
 	assert(nsec3_nodes);
@@ -583,7 +583,7 @@ static void reset_nodes(const zone_contents_t *zone)
  * \brief Create new NSEC3 chain, add differences from current into a changeset.
  */
 int knot_nsec3_create_chain(const zone_contents_t *zone, uint32_t ttl,
-                            knot_changeset_t *changeset)
+                            changeset_t *changeset)
 {
 	assert(zone);
 	assert(changeset);
diff --git a/src/knot/dnssec/nsec3-chain.h b/src/knot/dnssec/nsec3-chain.h
index 2238a09cf15e6aa829c26d46a3e88144e932461a..defeaf60eac980c84dd98819daf4ac99a8177244 100644
--- a/src/knot/dnssec/nsec3-chain.h
+++ b/src/knot/dnssec/nsec3-chain.h
@@ -40,6 +40,6 @@
  * \return KNOT_E*
  */
 int knot_nsec3_create_chain(const zone_contents_t *zone, uint32_t ttl,
-                            knot_changeset_t *changeset);
+                            changeset_t *changeset);
 
 #endif // _KNOT_DNSSEC_NSEC3_CHAIN_FIX_H_
diff --git a/src/knot/dnssec/zone-events.c b/src/knot/dnssec/zone-events.c
index c3f02f3832f9ac8115c6c304bb9b2107507a5d55..e4713a96efdd28f10f84448648c2ed58bb466cdb 100644
--- a/src/knot/dnssec/zone-events.c
+++ b/src/knot/dnssec/zone-events.c
@@ -64,7 +64,7 @@ static int init_dnssec_structs(const zone_contents_t *zone,
 }
 
 static int zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
-                     knot_changeset_t *out_ch, bool force,
+                     changeset_t *out_ch, bool force,
                      knot_update_serial_t soa_up, uint32_t *refresh_at)
 {
 	assert(zone);
@@ -79,7 +79,7 @@ static int zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
 	uint32_t new_serial = zone_contents_next_serial(zone, zone_config->serial_policy);
 
 	dbg_dnssec_verb("Changeset empty before generating NSEC chain: %d\n",
-	                knot_changeset_is_empty(out_ch));
+	                changeset_is_empty(out_ch));
 
 	// Init needed structs
 	knot_zone_keys_t zone_keys = { '\0' };
@@ -102,7 +102,7 @@ static int zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
 		return result;
 	}
 	dbg_dnssec_verb("Changeset empty after generating NSEC chain: %d\n",
-	                knot_changeset_is_empty(out_ch));
+	                changeset_is_empty(out_ch));
 
 	// add missing signatures
 	result = knot_zone_sign(zone, &zone_keys, &policy, out_ch,
@@ -115,16 +115,16 @@ static int zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
 		return result;
 	}
 	dbg_dnssec_verb("Changeset emtpy after signing: %d\n",
-	                knot_changeset_is_empty(out_ch));
+	                changeset_is_empty(out_ch));
 
 	// Check if only SOA changed
-	if (knot_changeset_is_empty(out_ch) &&
+	if (changeset_is_empty(out_ch) &&
 	    !knot_zone_sign_soa_expired(zone, &zone_keys, &policy)) {
 		log_zone_info("%s No signing performed, zone is valid.\n",
 		              msgpref);
 		free(msgpref);
 		knot_free_zone_keys(&zone_keys);
-		assert(knot_changeset_is_empty(out_ch));
+		assert(changeset_is_empty(out_ch));
 		return KNOT_EOK;
 	}
 
@@ -144,7 +144,7 @@ static int zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
 
 	knot_free_zone_keys(&zone_keys);
 	dbg_dnssec_detail("Zone signed: changes=%zu\n",
-	                  knot_changeset_size(out_ch));
+	                  changeset_size(out_ch));
 
 	log_zone_info("%s Successfully signed.\n", msgpref);
 	free(msgpref);
@@ -153,7 +153,7 @@ static int zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
 }
 
 int knot_dnssec_zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
-                          knot_changeset_t *out_ch,
+                          changeset_t *out_ch,
                           knot_update_serial_t soa_up, uint32_t *refresh_at)
 {
 	if (zone == NULL || zone_config == NULL || out_ch == NULL) {
@@ -164,7 +164,7 @@ int knot_dnssec_zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
 }
 
 int knot_dnssec_zone_sign_force(zone_contents_t *zone, const conf_zone_t *zone_config,
-                                knot_changeset_t *out_ch, uint32_t *refresh_at)
+                                changeset_t *out_ch, uint32_t *refresh_at)
 {
 	if (zone == NULL || zone_config == NULL || out_ch == NULL) {
 		return KNOT_EINVAL;
@@ -175,8 +175,8 @@ int knot_dnssec_zone_sign_force(zone_contents_t *zone, const conf_zone_t *zone_c
 
 int knot_dnssec_sign_changeset(const zone_contents_t *zone,
                                conf_zone_t *zone_config,
-                               const knot_changeset_t *in_ch,
-                               knot_changeset_t *out_ch,
+                               const changeset_t *in_ch,
+                               changeset_t *out_ch,
                                uint32_t *refresh_at)
 {
 	if (!refresh_at || zone == NULL || in_ch == NULL || out_ch == NULL) {
diff --git a/src/knot/dnssec/zone-events.h b/src/knot/dnssec/zone-events.h
index c4ddd2dc0a951c162f0d4d7251cdd3665f3bbc03..e0f7667e6035c78d94d2d67d987363c2ec2c36c4 100644
--- a/src/knot/dnssec/zone-events.h
+++ b/src/knot/dnssec/zone-events.h
@@ -44,7 +44,7 @@
  * \return Error code, KNOT_EOK if successful.
  */
 int knot_dnssec_zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
-                          knot_changeset_t *out_ch,
+                          changeset_t *out_ch,
                           knot_update_serial_t soa_up, uint32_t *refresh_at);
 
 /*!
@@ -59,7 +59,7 @@ int knot_dnssec_zone_sign(zone_contents_t *zone, const conf_zone_t *zone_config,
  * \return Error code, KNOT_EOK if successful.
  */
 int knot_dnssec_zone_sign_force(zone_contents_t *zone, const conf_zone_t *zone_config,
-                                knot_changeset_t *out_ch,
+                                changeset_t *out_ch,
                                 uint32_t *refresh_at);
 
 /*!
@@ -76,8 +76,8 @@ int knot_dnssec_zone_sign_force(zone_contents_t *zone, const conf_zone_t *zone_c
  */
 int knot_dnssec_sign_changeset(const zone_contents_t *zone,
                                conf_zone_t *zone_config,
-                               const knot_changeset_t *in_ch,
-                               knot_changeset_t *out_ch,
+                               const changeset_t *in_ch,
+                               changeset_t *out_ch,
                                uint32_t *refresh_at);
 
 #endif // _KNOT_DNSSEC_ZONE_EVENTS_H_
diff --git a/src/knot/dnssec/zone-nsec.c b/src/knot/dnssec/zone-nsec.c
index aacd7226cd32a63366b031ef573371fbdb5e075d..6f9fa2a9d3b23d4b6374140b96ce69a74280405d 100644
--- a/src/knot/dnssec/zone-nsec.c
+++ b/src/knot/dnssec/zone-nsec.c
@@ -44,7 +44,7 @@
  * \return KNOT_E*
  */
 static int delete_nsec3_chain(const zone_contents_t *zone,
-                              knot_changeset_t *changeset)
+                              changeset_t *changeset)
 {
 	assert(zone);
 	assert(changeset);
@@ -149,14 +149,14 @@ static int mark_nsec3(knot_rrset_t *rrset, void *data)
  * For each NSEC3 RRSet in the changeset finds its node and marks it with the
  * 'removed' flag.
  */
-static int mark_removed_nsec3(knot_changeset_t *out_ch,
+static int mark_removed_nsec3(changeset_t *out_ch,
                               const zone_contents_t *zone)
 {
 	if (knot_zone_tree_is_empty(zone->nsec3_nodes)) {
 		return KNOT_EOK;
 	}
 
-	int ret = knot_changeset_apply(out_ch, KNOT_CHANGESET_REMOVE,
+	int ret = changeset_apply(out_ch, CHANGESET_REMOVE,
 	                               mark_nsec3, (void *)zone->nsec3_nodes);
 	return ret;
 }
@@ -246,7 +246,7 @@ knot_dname_t *knot_nsec3_hash_to_dname(const uint8_t *hash, size_t hash_size,
  * \brief Create NSEC or NSEC3 chain in the zone.
  */
 int knot_zone_create_nsec_chain(const zone_contents_t *zone,
-                                knot_changeset_t *changeset,
+                                changeset_t *changeset,
                                 const knot_zone_keys_t *zone_keys,
                                 const knot_dnssec_policy_t *policy)
 {
diff --git a/src/knot/dnssec/zone-nsec.h b/src/knot/dnssec/zone-nsec.h
index 65bb0546540f394041de775187fbd8d1ddd0364e..8fc569da575b42a85a3386cfc1b1cedc01aef4cf 100644
--- a/src/knot/dnssec/zone-nsec.h
+++ b/src/knot/dnssec/zone-nsec.h
@@ -81,7 +81,7 @@ knot_dname_t *knot_create_nsec3_owner(const knot_dname_t *owner,
  * \return Error code, KNOT_EOK if successful.
  */
 int knot_zone_create_nsec_chain(const zone_contents_t *zone,
-                                knot_changeset_t *changeset,
+                                changeset_t *changeset,
                                 const knot_zone_keys_t *zone_keys,
                                 const knot_dnssec_policy_t *policy);
 
diff --git a/src/knot/dnssec/zone-sign.c b/src/knot/dnssec/zone-sign.c
index d92bd3035d561db5e43e3fca0a7e5f7fc1ead2a4..72963ccc30ce0fceb9e0f642838c09b756be9145 100644
--- a/src/knot/dnssec/zone-sign.c
+++ b/src/knot/dnssec/zone-sign.c
@@ -212,7 +212,7 @@ static int remove_expired_rrsigs(const knot_rrset_t *covered,
                                  const knot_rrset_t *rrsigs,
                                  const knot_zone_keys_t *zone_keys,
                                  const knot_dnssec_policy_t *policy,
-                                 knot_changeset_t *changeset,
+                                 changeset_t *changeset,
                                  uint32_t *expires_at)
 {
 	assert(changeset);
@@ -274,8 +274,8 @@ static int remove_expired_rrsigs(const knot_rrset_t *covered,
 	}
 
 	if (to_remove != NULL && result == KNOT_EOK) {
-		result = knot_changeset_add_rrset(changeset, to_remove,
-		                                  KNOT_CHANGESET_REMOVE);
+		result = changeset_add_rrset(changeset, to_remove,
+		                                  CHANGESET_REMOVE);
 	}
 
 	if (to_remove != NULL && result != KNOT_EOK) {
@@ -302,7 +302,7 @@ static int add_missing_rrsigs(const knot_rrset_t *covered,
                               const knot_rrset_t *rrsigs,
                               const knot_zone_keys_t *zone_keys,
                               const knot_dnssec_policy_t *policy,
-                              knot_changeset_t *changeset)
+                              changeset_t *changeset)
 {
 	assert(!knot_rrset_empty(covered));
 	assert(zone_keys);
@@ -337,8 +337,8 @@ static int add_missing_rrsigs(const knot_rrset_t *covered,
 	}
 
 	if (to_add != NULL && result == KNOT_EOK) {
-		result = knot_changeset_add_rrset(changeset, to_add,
-		                                  KNOT_CHANGESET_ADD);
+		result = changeset_add_rrset(changeset, to_add,
+		                                  CHANGESET_ADD);
 	}
 
 	if (to_add != NULL && result != KNOT_EOK) {
@@ -358,7 +358,7 @@ static int add_missing_rrsigs(const knot_rrset_t *covered,
  */
 static int remove_rrset_rrsigs(const knot_dname_t *owner, uint16_t type,
                                const knot_rrset_t *rrsigs,
-                               knot_changeset_t *changeset)
+                               changeset_t *changeset)
 {
 	assert(owner);
 	assert(changeset);
@@ -376,8 +376,8 @@ static int remove_rrset_rrsigs(const knot_dname_t *owner, uint16_t type,
 		return KNOT_EOK;
 	}
 
-	ret = knot_changeset_add_rrset(changeset, synth_rrsig,
-	                               KNOT_CHANGESET_REMOVE);
+	ret = changeset_add_rrset(changeset, synth_rrsig,
+	                               CHANGESET_REMOVE);
 	if (ret != KNOT_EOK) {
 		knot_rrset_free(&synth_rrsig, NULL);
 	}
@@ -399,7 +399,7 @@ static int force_resign_rrset(const knot_rrset_t *covered,
                               const knot_rrset_t *rrsigs,
                               const knot_zone_keys_t *zone_keys,
                               const knot_dnssec_policy_t *policy,
-                              knot_changeset_t *changeset)
+                              changeset_t *changeset)
 {
 	assert(!knot_rrset_empty(covered));
 
@@ -429,7 +429,7 @@ static int resign_rrset(const knot_rrset_t *covered,
                         const knot_rrset_t *rrsigs,
                         const knot_zone_keys_t *zone_keys,
                         const knot_dnssec_policy_t *policy,
-                        knot_changeset_t *changeset,
+                        changeset_t *changeset,
                         uint32_t *expires_at)
 {
 	assert(!knot_rrset_empty(covered));
@@ -453,7 +453,7 @@ static int resign_rrset(const knot_rrset_t *covered,
 
 static int remove_standalone_rrsigs(const zone_node_t *node,
                                     const knot_rrset_t *rrsigs,
-                                    knot_changeset_t *changeset)
+                                    changeset_t *changeset)
 {
 	if (rrsigs == NULL) {
 		return KNOT_EOK;
@@ -476,8 +476,8 @@ static int remove_standalone_rrsigs(const zone_node_t *node,
 				knot_rrset_free(&to_remove, NULL);
 				return ret;
 			}
-			ret = knot_changeset_add_rrset(changeset,
-			                               to_remove, KNOT_CHANGESET_REMOVE);
+			ret = changeset_add_rrset(changeset,
+			                               to_remove, CHANGESET_REMOVE);
 			if (ret != KNOT_EOK) {
 				knot_rrset_free(&to_remove, NULL);
 				return ret;
@@ -502,7 +502,7 @@ static int remove_standalone_rrsigs(const zone_node_t *node,
 static int sign_node_rrsets(const zone_node_t *node,
                             const knot_zone_keys_t *zone_keys,
                             const knot_dnssec_policy_t *policy,
-                            knot_changeset_t *changeset,
+                            changeset_t *changeset,
                             uint32_t *expires_at)
 {
 	assert(node);
@@ -548,7 +548,7 @@ static int sign_node_rrsets(const zone_node_t *node,
 typedef struct node_sign_args {
 	const knot_zone_keys_t *zone_keys;
 	const knot_dnssec_policy_t *policy;
-	knot_changeset_t *changeset;
+	changeset_t *changeset;
 	uint32_t expires_at;
 } node_sign_args_t;
 
@@ -594,7 +594,7 @@ static int sign_node(zone_node_t **node, void *data)
 static int zone_tree_sign(knot_zone_tree_t *tree,
                           const knot_zone_keys_t *zone_keys,
                           const knot_dnssec_policy_t *policy,
-                          knot_changeset_t *changeset,
+                          changeset_t *changeset,
                           uint32_t *expires_at)
 {
 	assert(zone_keys);
@@ -623,7 +623,7 @@ typedef struct {
 	const zone_contents_t *zone;
 	const knot_zone_keys_t *zone_keys;
 	const knot_dnssec_policy_t *policy;
-	knot_changeset_t *changeset;
+	changeset_t *changeset;
 	hattrie_t *signed_tree;
 } changeset_signing_data_t;
 
@@ -738,7 +738,7 @@ static int rrset_add_zone_key(knot_rrset_t *rrset,
 static int remove_invalid_dnskeys(const knot_rrset_t *soa,
                                   const knot_rrset_t *dnskeys,
                                   const knot_zone_keys_t *zone_keys,
-                                  knot_changeset_t *changeset)
+                                  changeset_t *changeset)
 {
 	assert(soa->type == KNOT_RRTYPE_SOA);
 	assert(changeset);
@@ -802,8 +802,8 @@ static int remove_invalid_dnskeys(const knot_rrset_t *soa,
 done:
 
 	if (to_remove != NULL && result == KNOT_EOK) {
-		result = knot_changeset_add_rrset(changeset, to_remove,
-		                                  KNOT_CHANGESET_REMOVE);
+		result = changeset_add_rrset(changeset, to_remove,
+		                                  CHANGESET_REMOVE);
 	}
 
 	if (to_remove != NULL && result != KNOT_EOK) {
@@ -840,7 +840,7 @@ static knot_rrset_t *create_dnskey_rrset_from_soa(const knot_rrset_t *soa)
 static int add_missing_dnskeys(const knot_rrset_t *soa,
                                const knot_rrset_t *dnskeys,
                                const knot_zone_keys_t *zone_keys,
-                               knot_changeset_t *changeset)
+                               changeset_t *changeset)
 {
 	assert(soa);
 	assert(soa->type == KNOT_RRTYPE_SOA);
@@ -883,8 +883,8 @@ static int add_missing_dnskeys(const knot_rrset_t *soa,
 	}
 
 	if (to_add != NULL && result == KNOT_EOK) {
-		result = knot_changeset_add_rrset(changeset, to_add,
-		                                  KNOT_CHANGESET_ADD);
+		result = changeset_add_rrset(changeset, to_add,
+		                                  CHANGESET_ADD);
 	}
 
 	if (to_add != NULL && result != KNOT_EOK) {
@@ -910,7 +910,7 @@ static int update_dnskeys_rrsigs(const knot_rrset_t *dnskeys,
                                  const knot_rrset_t *soa,
                                  const knot_zone_keys_t *zone_keys,
                                  const knot_dnssec_policy_t *policy,
-                                 knot_changeset_t *changeset)
+                                 changeset_t *changeset)
 {
 	assert(zone_keys);
 	assert(changeset);
@@ -988,7 +988,7 @@ fail:
 static int update_dnskeys(const zone_contents_t *zone,
                           const knot_zone_keys_t *zone_keys,
                           const knot_dnssec_policy_t *policy,
-                          knot_changeset_t *changeset)
+                          changeset_t *changeset)
 {
 	assert(zone);
 	assert(zone->apex);
@@ -1003,7 +1003,7 @@ static int update_dnskeys(const zone_contents_t *zone,
 	}
 
 	int result;
-	size_t changes_before = knot_changeset_size(changeset);
+	size_t changes_before = changeset_size(changeset);
 
 	result = remove_invalid_dnskeys(&soa, &dnskeys, zone_keys, changeset);
 	if (result != KNOT_EOK) {
@@ -1025,7 +1025,7 @@ static int update_dnskeys(const zone_contents_t *zone,
 		}
 	}
 
-	bool modified = (knot_changeset_size(changeset) != changes_before);
+	bool modified = (changeset_size(changeset) != changes_before);
 	bool signatures_exist = (!knot_rrset_empty(&dnskeys) &&
 	                        all_signatures_exist(&dnskeys, &dnskey_rrsig,
 	                                             zone_keys, policy));
@@ -1259,7 +1259,7 @@ static void knot_zone_clear_sorted_changes(hattrie_t *t)
 int knot_zone_sign(const zone_contents_t *zone,
                    const knot_zone_keys_t *zone_keys,
                    const knot_dnssec_policy_t *policy,
-                   knot_changeset_t *changeset,
+                   changeset_t *changeset,
                    uint32_t *refresh_at)
 {
 	if (!zone || !zone_keys || !policy || !changeset || !refresh_at) {
@@ -1327,7 +1327,7 @@ int knot_zone_sign_update_soa(const knot_rrset_t *soa,
                               const knot_zone_keys_t *zone_keys,
                               const knot_dnssec_policy_t *policy,
                               uint32_t new_serial,
-                              knot_changeset_t *changeset)
+                              changeset_t *changeset)
 {
 	if (knot_rrset_empty(soa) || !zone_keys || !policy || !changeset) {
 		return KNOT_EINVAL;
@@ -1404,8 +1404,8 @@ int knot_zone_sign_update_soa(const knot_rrset_t *soa,
  * \brief Sign changeset created by DDNS or zone-diff.
  */
 int knot_zone_sign_changeset(const zone_contents_t *zone,
-                             const knot_changeset_t *in_ch,
-                             knot_changeset_t *out_ch,
+                             const changeset_t *in_ch,
+                             changeset_t *out_ch,
                              const knot_zone_keys_t *zone_keys,
                              const knot_dnssec_policy_t *policy)
 {
@@ -1424,14 +1424,14 @@ int knot_zone_sign_changeset(const zone_contents_t *zone,
 	}
 
 	// Sign all RRs that are new in changeset
-	int ret = knot_changeset_apply((knot_changeset_t *)in_ch,
-	                               KNOT_CHANGESET_ADD,
+	int ret = changeset_apply((changeset_t *)in_ch,
+	                               CHANGESET_ADD,
 	                               sign_changeset_wrap, &args);
 
 	// Sign all RRs that are removed in changeset
 	if (ret == KNOT_EOK) {
-		ret = knot_changeset_apply((knot_changeset_t *)in_ch,
-		                           KNOT_CHANGESET_REMOVE,
+		ret = changeset_apply((changeset_t *)in_ch,
+		                           CHANGESET_REMOVE,
 		                           sign_changeset_wrap, &args);
 	}
 
@@ -1446,7 +1446,7 @@ int knot_zone_sign_changeset(const zone_contents_t *zone,
  */
 int knot_zone_sign_nsecs_in_changeset(const knot_zone_keys_t *zone_keys,
                                       const knot_dnssec_policy_t *policy,
-                                      knot_changeset_t *changeset)
+                                      changeset_t *changeset)
 {
 	assert(zone_keys);
 	assert(policy);
@@ -1457,7 +1457,7 @@ int knot_zone_sign_nsecs_in_changeset(const knot_zone_keys_t *zone_keys,
 	                                 .policy = policy,
 	                                 .changeset = changeset };
 
-	return knot_changeset_apply(changeset, KNOT_CHANGESET_ADD,
+	return changeset_apply(changeset, CHANGESET_ADD,
 	                            add_rrsigs_for_nsec, &data);
 }
 
diff --git a/src/knot/dnssec/zone-sign.h b/src/knot/dnssec/zone-sign.h
index a986e425e52e44f77b38df338f6ba6c0026b30a9..dd6831749bfa85f4ad7a7630125416f80b0c5175 100644
--- a/src/knot/dnssec/zone-sign.h
+++ b/src/knot/dnssec/zone-sign.h
@@ -62,7 +62,7 @@ typedef struct signed_info {
 int knot_zone_sign(const zone_contents_t *zone,
                    const knot_zone_keys_t *zone_keys,
                    const knot_dnssec_policy_t *policy,
-                   knot_changeset_t *out_ch, uint32_t *refresh_at);
+                   changeset_t *out_ch, uint32_t *refresh_at);
 
 /*!
  * \brief Update and sign SOA and store performed changes in changeset.
@@ -77,7 +77,7 @@ int knot_zone_sign(const zone_contents_t *zone,
 int knot_zone_sign_update_soa(const knot_rrset_t *soa, const knot_rrset_t *rrsigs,
                               const knot_zone_keys_t *zone_keys,
                               const knot_dnssec_policy_t *policy,
-                              uint32_t new_serial, knot_changeset_t *changeset);
+                              uint32_t new_serial, changeset_t *changeset);
 
 /*!
  * \brief Check if zone SOA signatures are expired.
@@ -105,8 +105,8 @@ bool knot_zone_sign_soa_expired(const zone_contents_t *zone,
  * \return Error code, KNOT_EOK if successful.
  */
 int knot_zone_sign_changeset(const zone_contents_t *zone,
-                             const knot_changeset_t *in_ch,
-                             knot_changeset_t *out_ch,
+                             const changeset_t *in_ch,
+                             changeset_t *out_ch,
                              const knot_zone_keys_t *zone_keys,
                              const knot_dnssec_policy_t *policy);
 
@@ -121,7 +121,7 @@ int knot_zone_sign_changeset(const zone_contents_t *zone,
  */
 int knot_zone_sign_nsecs_in_changeset(const knot_zone_keys_t *zone_keys,
                                       const knot_dnssec_policy_t *policy,
-                                      knot_changeset_t *changeset);
+                                      changeset_t *changeset);
 
 /*!
  * \brief Checks whether RRSet in a node has to be signed. Will not return
diff --git a/src/knot/nameserver/axfr.c b/src/knot/nameserver/axfr.c
index 4d4464ac781d20bdf41493ee45776979b73d24ec..a4656ce4fd8a2194d47c7c1006010ec1cc39c3b6 100644
--- a/src/knot/nameserver/axfr.c
+++ b/src/knot/nameserver/axfr.c
@@ -18,7 +18,7 @@
 #include "knot/nameserver/internet.h"
 #include "knot/nameserver/process_query.h"
 #include "knot/nameserver/process_answer.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "knot/zone/zonefile.h"
 #include "common/debug.h"
 #include "common/descriptor.h"
@@ -225,7 +225,7 @@ int axfr_query(knot_pkt_t *pkt, struct query_data *qdata)
 		return NS_PROC_DONE;
 		break;
 	default:          /* Generic error. */
-		AXFROUT_LOG(LOG_ERR, "%s", knot_strerror(ret));
+		AXFROUT_LOG(LOG_ERR, "Failed: %s", knot_strerror(ret));
 		return NS_PROC_FAIL;
 	}
 }
@@ -235,7 +235,7 @@ static void axfr_answer_cleanup(struct answer_data *data)
 {
 	struct xfr_proc *proc = data->ext;
 	if (proc) {
-		zone_contents_deep_free(&proc->zone);
+		zone_contents_deep_free(&proc->contents);
 		mm_free(data->mm, proc);
 		data->ext = NULL;
 	}
@@ -260,7 +260,7 @@ static int axfr_answer_init(struct answer_data *data)
 	}
 
 	memset(proc, 0, sizeof(struct xfr_proc));
-	proc->zone = new_contents;
+	proc->contents = new_contents;
 	gettimeofday(&proc->tstamp, NULL);
 
 	/* Set up cleanup callback. */
@@ -284,23 +284,24 @@ static int axfr_answer_finalize(struct answer_data *data)
 	 * marked authoritative / delegation point.
 	 */
 	struct xfr_proc *proc = data->ext;
-	int rc = zone_contents_adjust_full(proc->zone, NULL, NULL);
+	int rc = zone_contents_adjust_full(proc->contents, NULL, NULL);
 	if (rc != KNOT_EOK) {
 		return rc;
 	}
 
 	/* Write zone file. */
 	zone_t *zone = data->param->zone;
-	rc = zonefile_write(zone->conf->file, proc->zone, data->param->remote);
+	rc = zonefile_write(zone->conf->file, proc->contents, data->param->remote);
 	if (rc != KNOT_EOK) {
 		return rc;
 	}
 
 	/* Switch contents. */
-	zone_contents_t *old_contents = xfrin_switch_zone(zone, proc->zone);
+	zone_contents_t *old_contents = zone_switch_contents(zone, proc->contents);
+	synchronize_rcu();
 	AXFRIN_LOG(LOG_INFO, "Serial %u -> %u",
 	           zone_contents_serial(old_contents),
-	           zone_contents_serial(proc->zone));
+	           zone_contents_serial(proc->contents));
 
 	AXFRIN_LOG(LOG_INFO, "Finished in %.02fs (%u messages, ~%.01fkB).",
 	         time_diff(&proc->tstamp, &now) / 1000.0,
@@ -308,17 +309,49 @@ static int axfr_answer_finalize(struct answer_data *data)
 
 	/* Do not free new contents with cleanup. */
 	zone_contents_deep_free(&old_contents);
-	proc->zone = NULL;
+	proc->contents = NULL;
 
 	return KNOT_EOK;
 }
 
+static int process_axfr_packet(knot_pkt_t *pkt, struct xfr_proc *proc)
+{
+	if (pkt == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	++proc->npkts;
+
+	// Init zone creator
+	zcreator_t zc = {.z = proc->contents, .master = false, .ret = KNOT_EOK };
+
+	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+	for (uint16_t i = 0; i < answer->count; ++i) {
+		const knot_rrset_t *rr = &answer->rr[i];
+		if (rr->type == KNOT_RRTYPE_SOA &&
+		    node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) {
+			return NS_PROC_DONE;
+		} else {
+			int ret = zcreator_step(&zc, rr);
+			if (ret != KNOT_EOK) {
+				return NS_PROC_FAIL;
+			}
+		}
+	}
+
+	return NS_PROC_MORE;
+}
+
 int axfr_process_answer(knot_pkt_t *pkt, struct answer_data *data)
 {
 	/* Initialize processing with first packet. */
 	int ret = KNOT_EOK;
 	if (data->ext == NULL) {
 		NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 0);
+		if (!zone_transfer_needed(data->param->zone, pkt)) {
+			AXFRIN_LOG(LOG_INFO, "Zone is up-to-date.");
+			return NS_PROC_DONE;
+		}
 
 		ret = axfr_answer_init(data);
 		if (ret != KNOT_EOK) {
@@ -329,26 +362,19 @@ int axfr_process_answer(knot_pkt_t *pkt, struct answer_data *data)
 	}
 
 	/* Process answer packet. */
-	ret = xfrin_process_axfr_packet(pkt, (struct xfr_proc *)data->ext);
-#warning TODO: this function wraps the old processing interface, hence the retval conversion below
-	if (ret > 0) {
+	ret = process_axfr_packet(pkt, (struct xfr_proc *)data->ext);
+	if (ret == NS_PROC_DONE) {
 		NS_NEED_TSIG_SIGNED(&data->param->tsig_ctx, 0);
-
 		/* This was the last packet, finalize zone and publish it. */
 		ret = axfr_answer_finalize(data);
 		if (ret != KNOT_EOK) {
 			return NS_PROC_FAIL;
 		}
 
-		return NS_PROC_DONE;
-	}
-
-	if (ret != KNOT_EOK) {
-		fprintf(stderr, "%s:%d\n", __FILE__, __LINE__);
-		return NS_PROC_FAIL;
+		return ret;
 	}
 
-	return NS_PROC_MORE;
+	return ret;
 }
 
 #undef AXFR_QRLOG
diff --git a/src/knot/nameserver/axfr.h b/src/knot/nameserver/axfr.h
index ff4aba5b6ceae4431c7003a6d7791d607b0cef03..1be08c583b89eda64caf8bb6955eb870b3c63b9d 100644
--- a/src/knot/nameserver/axfr.h
+++ b/src/knot/nameserver/axfr.h
@@ -39,7 +39,7 @@ struct xfr_proc {
 	unsigned npkts;  /* Packets processed. */
 	unsigned nbytes; /* Bytes processed. */
 	struct timeval tstamp; /* Start time. */
-	zone_contents_t *zone; /* Processed zone. */
+	zone_contents_t *contents; /* Processed zone. */
 };
 
 /*! \brief Generic transfer processing (reused for IXFR).
diff --git a/src/knot/nameserver/ixfr.c b/src/knot/nameserver/ixfr.c
index c01f603b2d772abc950f3f09e70b3e14cceb42ec..75bdaab7524f317a51e952e291b04aa8d32c3a1a 100644
--- a/src/knot/nameserver/ixfr.c
+++ b/src/knot/nameserver/ixfr.c
@@ -3,32 +3,39 @@
 #include "knot/nameserver/internet.h"
 #include "knot/nameserver/process_query.h"
 #include "knot/nameserver/process_answer.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "common/debug.h"
 #include "common/descriptor.h"
 #include "libknot/util/utils.h"
 #include "libknot/rdata/soa.h"
 
-/*! \brief Current IXFR answer sections. */
-enum {
-	SOA_REMOVE = 0,
-	REMOVE,
-	SOA_ADD,
-	ADD
+/* ------------------------ IXFR-out processing ----------------------------- */
+
+/*! \brief IXFR-in processing states. */
+enum ixfr_states {
+	IXFR_START = 0,  /* IXFR-in starting, expecting final SOA. */
+	IXFR_SOA_DEL,    /* Expecting starting SOA. */
+	IXFR_SOA_ADD,    /* Expecting ending SOA. */
+	IXFR_DEL,        /* Expecting RR to delete. */
+	IXFR_ADD,        /* Expecting RR to add. */
+	IXFR_DONE        /* Processing done, IXFR-in complete. */
 };
 
-/*! \brief Extended structure for IXFR processing. */
+/*! \brief Extended structure for IXFR-in/IXFR-out processing. */
 struct ixfr_proc {
-	struct xfr_proc proc;
+	struct xfr_proc proc;          /* Generic transfer processing context. */
 	node_t *cur;
-	unsigned state;
-	knot_changesets_t *changesets;
+	int state;                     /* IXFR-in state. */
+	changesets_t *changesets;      /* Processed changesets. */
+	zone_t *zone;                  /* Modified zone - for journal access. */
+	mm_ctx_t *mm;                  /* Memory context for RR allocations. */
 	struct query_data *qdata;
-	const knot_rrset_t *soa_from, *soa_to;
+	const knot_rrset_t *soa_from;
+	const knot_rrset_t *soa_to;
 };
 
-/* IXFR-specific logging (internal, expects 'qdata' variable set). */
-#define IXFR_LOG(severity, msg...) \
+/* IXFR-out-specific logging (internal, expects 'qdata' variable set). */
+#define IXFROUT_LOG(severity, msg...) \
 	QUERY_LOG(severity, qdata, "Outgoing IXFR", msg)
 
 /*! \brief Helper macro for putting RRs into packet. */
@@ -74,56 +81,57 @@ static int ixfr_put_rrlist(knot_pkt_t *pkt, struct ixfr_proc *ixfr, list_t *list
  *       with next empty answer and it must resume the processing exactly where
  *       it's left off.
  */
-static int ixfr_process_changeset(knot_pkt_t *pkt, const void *item, struct xfr_proc *xfer)
+static int ixfr_process_changeset(knot_pkt_t *pkt, const void *item,
+                                  struct xfr_proc *xfer)
 {
 	int ret = KNOT_EOK;
 	struct ixfr_proc *ixfr = (struct ixfr_proc *)xfer;
-	knot_changeset_t *chgset = (knot_changeset_t *)item;
+	changeset_t *chgset = (changeset_t *)item;
 
 	/* Put former SOA. */
-	if (ixfr->state == SOA_REMOVE) {
+	if (ixfr->state == IXFR_SOA_DEL) {
 		IXFR_SAFE_PUT(pkt, chgset->soa_from);
 		dbg_ns("%s: put 'REMOVE' SOA\n", __func__);
-		ixfr->state = REMOVE;
+		ixfr->state = IXFR_DEL;
 	}
 
 	/* Put REMOVE RRSets. */
-	if (ixfr->state == REMOVE) {
+	if (ixfr->state == IXFR_DEL) {
 		ret = ixfr_put_rrlist(pkt, ixfr, &chgset->remove);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
 		dbg_ns("%s: put 'REMOVE' RRs\n", __func__);
-		ixfr->state = SOA_ADD;
+		ixfr->state = IXFR_SOA_ADD;
 	}
 
 	/* Put next SOA. */
-	if (ixfr->state == SOA_ADD) {
+	if (ixfr->state == IXFR_SOA_ADD) {
 		IXFR_SAFE_PUT(pkt, chgset->soa_to);
-		dbg_ns("%s: put 'ADD' SOA\n", __func__);
-		ixfr->state = ADD;
+		dbg_ns("%s: put 'IXFR_ADD' SOA\n", __func__);
+		ixfr->state = IXFR_ADD;
 	}
 
 	/* Put REMOVE RRSets. */
-	if (ixfr->state == ADD) {
+	if (ixfr->state == IXFR_ADD) {
 		ret = ixfr_put_rrlist(pkt, ixfr, &chgset->add);
 		if (ret != KNOT_EOK) {
 			return ret;
 		}
-		dbg_ns("%s: put 'ADD' RRs\n", __func__);
-		ixfr->state = SOA_REMOVE;
+		dbg_ns("%s: put 'IXFR_ADD' RRs\n", __func__);
+		ixfr->state = IXFR_SOA_DEL;
 	}
 
 	/* Finished change set. */
-	struct query_data *qdata = ixfr->qdata; /*< Required for IXFR_LOG() */
-	IXFR_LOG(LOG_INFO, "Serial %u -> %u.", chgset->serial_from, chgset->serial_to);
+	struct query_data *qdata = ixfr->qdata; /*< Required for IXFROUT_LOG() */
+	IXFROUT_LOG(LOG_INFO, "Serial %u -> %u.", chgset->serial_from, chgset->serial_to);
 
 	return ret;
 }
 
 #undef IXFR_SAFE_PUT
 
-static int ixfr_load_chsets(knot_changesets_t **chgsets, const zone_t *zone,
+static int ixfr_load_chsets(changesets_t **chgsets, const zone_t *zone,
 			    const knot_rrset_t *their_soa)
 {
 	assert(chgsets);
@@ -137,7 +145,7 @@ static int ixfr_load_chsets(knot_changesets_t **chgsets, const zone_t *zone,
 		return KNOT_EUPTODATE;
 	}
 
-	*chgsets = knot_changesets_create(0);
+	*chgsets = changesets_create(0);
 	if (*chgsets == NULL) {
 		return KNOT_ENOMEM;
 	}
@@ -145,7 +153,7 @@ static int ixfr_load_chsets(knot_changesets_t **chgsets, const zone_t *zone,
 	ret = journal_load_changesets(zone->conf->ixfr_db, *chgsets,
 	                              serial_from, serial_to);
 	if (ret != KNOT_EOK) {
-		knot_changesets_free(chgsets);
+		changesets_free(chgsets, NULL);
 	}
 
 	return ret;
@@ -181,7 +189,7 @@ static void ixfr_answer_cleanup(struct query_data *qdata)
 	mm_ctx_t *mm = qdata->mm;
 
 	ptrlist_free(&ixfr->proc.nodes, mm);
-	knot_changesets_free(&ixfr->changesets);
+	changesets_free(&ixfr->changesets, NULL);
 	mm->free(qdata->ext);
 
 	/* Allow zone changes (finished). */
@@ -202,7 +210,7 @@ static int ixfr_answer_init(struct query_data *qdata)
 
 	/* Compare serials. */
 	const knot_rrset_t *their_soa = &knot_pkt_section(qdata->query, KNOT_AUTHORITY)->rr[0];
-	knot_changesets_t *chgsets = NULL;
+	changesets_t *chgsets = NULL;
 	int ret = ixfr_load_chsets(&chgsets, qdata->zone, their_soa);
 	if (ret != KNOT_EOK) {
 		dbg_ns("%s: failed to load changesets => %d\n", __func__, ret);
@@ -213,17 +221,18 @@ static int ixfr_answer_init(struct query_data *qdata)
 	mm_ctx_t *mm = qdata->mm;
 	struct ixfr_proc *xfer = mm->alloc(mm->ctx, sizeof(struct ixfr_proc));
 	if (xfer == NULL) {
-		knot_changesets_free(&chgsets);
+		changesets_free(&chgsets, NULL);
 		return KNOT_ENOMEM;
 	}
 	memset(xfer, 0, sizeof(struct ixfr_proc));
 	gettimeofday(&xfer->proc.tstamp, NULL);
+	xfer->state = IXFR_SOA_DEL;
 	init_list(&xfer->proc.nodes);
 	xfer->qdata = qdata;
 
 	/* Put all changesets to processing queue. */
 	xfer->changesets = chgsets;
-	knot_changeset_t *chs = NULL;
+	changeset_t *chs = NULL;
 	WALK_LIST(chs, chgsets->sets) {
 		ptrlist_add(&xfer->proc.nodes, chs, mm);
 		dbg_ns("%s: preparing %u -> %u\n", __func__, chs->serial_from, chs->serial_to);
@@ -276,7 +285,283 @@ static int ixfr_answer_soa(knot_pkt_t *pkt, struct query_data *qdata)
 	return NS_PROC_DONE;
 }
 
-int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata)
+/* ------------------------- IXFR-in processing ----------------------------- */
+
+/* IXFR-in-specific logging (internal, expects 'adata' variable set). */
+#define IXFRIN_LOG(severity, msg...) \
+	ANSWER_LOG(severity, adata, "Incoming IXFR", msg)
+
+/*! \brief Cleans up data allocated by IXFR-in processing. */
+static void ixfrin_cleanup(struct answer_data *data)
+{
+	struct ixfr_proc *proc = data->ext;
+	if (proc) {
+		changesets_free(&proc->changesets, data->mm);
+		mm_free(data->mm, proc);
+		data->ext = NULL;
+	}
+}
+
+/*! \brief Initializes IXFR-in processing context. */
+static int ixfrin_answer_init(struct answer_data *data)
+{
+	struct ixfr_proc *proc = mm_alloc(data->mm, sizeof(struct ixfr_proc));
+	if (proc == NULL) {
+		return KNOT_ENOMEM;
+	}
+	memset(proc, 0, sizeof(struct ixfr_proc));
+	gettimeofday(&proc->proc.tstamp, NULL);
+
+	proc->changesets = changesets_create(0);
+	if (proc->changesets == NULL) {
+		mm_free(data->mm, proc);
+		return KNOT_ENOMEM;
+	}
+	proc->state = IXFR_START;
+	proc->zone = data->param->zone;
+	proc->mm = data->mm;
+
+	data->ext = proc;
+	data->ext_cleanup = &ixfrin_cleanup;
+
+	return KNOT_EOK;
+}
+
+/*! \brief Finalizes IXFR-in processing. */
+static int ixfrin_finalize(struct answer_data *adata)
+{
+	struct ixfr_proc *ixfr = adata->ext;
+
+	assert(ixfr->state == IXFR_DONE);
+	int ret = zone_change_apply_and_store(&ixfr->changesets,
+	                                      ixfr->zone, "IXFR", adata->mm);
+	if (ret != KNOT_EOK) {
+		IXFRIN_LOG(LOG_ERR, "Failed to apply changes to zone - %s",
+		           knot_strerror(ret));
+		ixfrin_cleanup(adata);
+		return ret;
+	}
+
+	struct timeval now = {0};
+	gettimeofday(&now, NULL);
+	IXFRIN_LOG(LOG_INFO, "Finished in %.02fs (%u messages, ~%.01fkB).",
+	           time_diff(&ixfr->proc.tstamp, &now) / 1000.0,
+	           ixfr->proc.npkts, ixfr->proc.nbytes / 1024.0);
+	ixfrin_cleanup(adata);
+
+	return KNOT_EOK;
+}
+
+/*! \brief Stores starting SOA into changesets structure. */
+static int solve_start(const knot_rrset_t *rr, changesets_t *changesets, mm_ctx_t *mm)
+{
+	assert(changesets->first_soa == NULL);
+	if (rr->type != KNOT_RRTYPE_SOA) {
+		return KNOT_EMALF;
+	}
+
+	// Store the first SOA for later use.
+	changesets->first_soa = knot_rrset_copy(rr, mm);
+	if (changesets->first_soa == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Decides what to do with a starting SOA (deletions). */
+static int solve_soa_del(const knot_rrset_t *rr, changesets_t *changesets,
+                         mm_ctx_t *mm)
+{
+	if (rr->type != KNOT_RRTYPE_SOA) {
+		return KNOT_EMALF;
+	}
+
+	// Create new changeset.
+	changeset_t *change = changesets_create_changeset(changesets);
+	if (change == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	// Store SOA into changeset.
+	change->soa_from = knot_rrset_copy(rr, mm);
+	if (change->soa_from == NULL) {
+		return KNOT_ENOMEM;
+	}
+	change->serial_from = knot_soa_serial(&rr->rrs);
+
+	return KNOT_EOK;
+}
+
+/*! \brief Stores ending SOA into changeset. */
+static int solve_soa_add(const knot_rrset_t *rr, changeset_t *change, mm_ctx_t *mm)
+{
+	assert(rr->type == KNOT_RRTYPE_SOA);
+	change->soa_to= knot_rrset_copy(rr, mm);
+	if (change->soa_to == NULL) {
+		return KNOT_ENOMEM;
+	}
+	change->serial_to = knot_soa_serial(&rr->rrs);
+
+	return KNOT_EOK;
+}
+
+/*! \brief Adds single RR into given section of changeset. */
+static int add_part(const knot_rrset_t *rr, changeset_t *change, int part, mm_ctx_t *mm)
+{
+	assert(rr->type != KNOT_RRTYPE_SOA);
+	knot_rrset_t *copy = knot_rrset_copy(rr, mm);
+	if (copy) {
+		int ret = changeset_add_rrset(change, copy, part);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+		return KNOT_EOK;
+	} else {
+		return KNOT_ENOMEM;
+	}
+}
+
+/*! \brief Adds single RR into remove section of changeset. */
+static int solve_del(const knot_rrset_t *rr, changeset_t *change, mm_ctx_t *mm)
+{
+	return add_part(rr, change, CHANGESET_REMOVE, mm);
+}
+
+/*! \brief Adds single RR into add section of changeset. */
+static int solve_add(const knot_rrset_t *rr, changeset_t *change, mm_ctx_t *mm)
+{
+	return add_part(rr, change, CHANGESET_ADD, mm);
+}
+
+/*! \brief Decides what the next IXFR-in state should be. */
+static int ixfrin_next_state(struct ixfr_proc *proc, const knot_rrset_t *rr)
+{
+	const bool soa = (rr->type == KNOT_RRTYPE_SOA);
+	if (soa &&
+	    (proc->state == IXFR_SOA_ADD || proc->state == IXFR_ADD)) {
+		// Check end of transfer.
+		if (knot_rrset_equal(rr, proc->changesets->first_soa,
+		                     KNOT_RRSET_COMPARE_WHOLE)) {
+			// Final SOA encountered, transfer done.
+			return IXFR_DONE;
+		}
+	}
+
+	switch (proc->state) {
+	case IXFR_START:
+		// Final SOA already stored or transfer start.
+		return proc->changesets->first_soa ? IXFR_SOA_DEL : IXFR_START;
+	case IXFR_SOA_DEL:
+		// Empty delete section or start of delete section.
+		return soa ? IXFR_SOA_ADD : IXFR_DEL;
+	case IXFR_SOA_ADD:
+		// Empty add section or start of add section.
+		return soa ? IXFR_SOA_DEL : IXFR_ADD;
+	case IXFR_DEL:
+		// End of delete section or continue.
+		return soa ? IXFR_SOA_ADD : IXFR_DEL;
+	case IXFR_ADD:
+		// End of add section or continue.
+		return soa ? IXFR_SOA_DEL : IXFR_ADD;
+	default:
+		assert(0);
+		return 0;
+	}
+}
+
+/*!
+ * \brief Processes single RR according to current IXFR-in state. The states
+ *        correspond with IXFR-in message structure, in the order they are
+ *        mentioned in the code.
+ *
+ * \param rr    RR to process.
+ * \param proc  Processing context.
+ *
+ * \return KNOT_E*
+ */
+static int ixfrin_step(const knot_rrset_t *rr, struct ixfr_proc *proc)
+{
+	proc->state = ixfrin_next_state(proc, rr);
+	changeset_t *change = changesets_get_last(proc->changesets);
+
+	switch (proc->state) {
+	case IXFR_START:
+		return solve_start(rr, proc->changesets, proc->mm);
+	case IXFR_SOA_DEL:
+		return solve_soa_del(rr, proc->changesets, proc->mm);
+	case IXFR_DEL:
+		return solve_del(rr, change, proc->mm);
+	case IXFR_SOA_ADD:
+		return solve_soa_add(rr, change, proc->mm);
+	case IXFR_ADD:
+		return solve_add(rr, change, proc->mm);
+	case IXFR_DONE:
+		return KNOT_EOK;
+	default:
+		return KNOT_ERROR;
+	}
+}
+
+/*! \brief Checks whether journal node limit has not been exceeded. */
+static bool journal_limit_exceeded(struct ixfr_proc *proc)
+{
+	return proc->changesets->count > JOURNAL_NCOUNT;
+}
+
+/*! \brief Checks whether RR belongs into zone. */
+static bool out_of_zone(const knot_rrset_t *rr, struct ixfr_proc *proc)
+{
+	return !knot_dname_is_sub(rr->owner, proc->zone->name) &&
+	       !knot_dname_is_equal(rr->owner, proc->zone->name);
+}
+
+/*!
+ * \brief Processes IXFR reply packet and fills in the changesets structure.
+ *
+ * \param pkt    Packet containing the IXFR reply in wire format.
+ * \param adata  Answer data, including processing context.
+ *
+ * \return NS_PROC_MORE, NS_PROC_DONE, NS_PROC_FAIL
+ */
+static int process_ixfrin_packet(knot_pkt_t *pkt, struct answer_data *adata)
+{
+	struct ixfr_proc *ixfr = (struct ixfr_proc *)adata->ext;
+	// Update counters.
+	ixfr->proc.npkts  += 1;
+	ixfr->proc.nbytes += pkt->size;
+
+	// Process RRs in the message.
+	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+	for (uint16_t i = 0; i < answer->count; ++i) {
+		if (journal_limit_exceeded(ixfr)) {
+			IXFRIN_LOG(LOG_WARNING, "Journal is full.");
+			return NS_PROC_FAIL;
+		}
+
+		const knot_rrset_t *rr = &answer->rr[i];
+		if (out_of_zone(rr, ixfr)) {
+			continue;
+		}
+
+		int ret = ixfrin_step(rr, ixfr);
+		if (ret != KNOT_EOK) {
+			IXFRIN_LOG(LOG_ERR, "Failed - %s", knot_strerror(ret));
+			return NS_PROC_FAIL;
+		}
+
+		if (ixfr->state == IXFR_DONE) {
+			// Transfer done, do not consume more RRs.
+			return NS_PROC_DONE;
+		}
+	}
+
+	return NS_PROC_MORE;
+}
+
+/* --------------------------------- API ------------------------------------ */
+
+int ixfr_query(knot_pkt_t *pkt, struct query_data *qdata)
 {
 	if (pkt == NULL || qdata == NULL) {
 		return NS_PROC_FAIL;
@@ -297,20 +582,20 @@ int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata)
 		switch(ret) {
 		case KNOT_EOK:      /* OK */
 			ixfr = (struct ixfr_proc*)qdata->ext;
-			IXFR_LOG(LOG_INFO, "Started (serial %u -> %u).",
-			         knot_soa_serial(&ixfr->soa_from->rrs),
-			         knot_soa_serial(&ixfr->soa_to->rrs));
+			IXFROUT_LOG(LOG_INFO, "Started (serial %u -> %u).",
+			            knot_soa_serial(&ixfr->soa_from->rrs),
+			            knot_soa_serial(&ixfr->soa_to->rrs));
 			break;
 		case KNOT_EUPTODATE: /* Our zone is same age/older, send SOA. */
-			IXFR_LOG(LOG_INFO, "Zone is up-to-date.");
+			IXFROUT_LOG(LOG_INFO, "Zone is up-to-date.");
 			return ixfr_answer_soa(pkt, qdata);
 		case KNOT_ERANGE:   /* No history -> AXFR. */
 		case KNOT_ENOENT:
-			IXFR_LOG(LOG_INFO, "Incomplete history, fallback to AXFR.");
+			IXFROUT_LOG(LOG_INFO, "Incomplete history, fallback to AXFR.");
 			qdata->packet_type = KNOT_QUERY_AXFR; /* Solve as AXFR. */
 			return axfr_query(pkt, qdata);
 		default:            /* Server errors. */
-			IXFR_LOG(LOG_ERR, "Failed to start (%s).", knot_strerror(ret));
+			IXFROUT_LOG(LOG_ERR, "Failed to start (%s).", knot_strerror(ret));
 			return NS_PROC_FAIL;
 		}
 	}
@@ -325,13 +610,13 @@ int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata)
 		return NS_PROC_FULL; /* Check for more. */
 	case KNOT_EOK:    /* Last response. */
 		gettimeofday(&now, NULL);
-		IXFR_LOG(LOG_INFO, "Finished in %.02fs (%u messages, ~%.01fkB).",
-		         time_diff(&ixfr->proc.tstamp, &now) / 1000.0,
-		         ixfr->proc.npkts, ixfr->proc.nbytes / 1024.0);
+		IXFROUT_LOG(LOG_INFO, "Finished in %.02fs (%u messages, ~%.01fkB).",
+		            time_diff(&ixfr->proc.tstamp, &now) / 1000.0,
+		            ixfr->proc.npkts, ixfr->proc.nbytes / 1024.0);
 		ret = NS_PROC_DONE;
 		break;
 	default:          /* Generic error. */
-		IXFR_LOG(LOG_ERR, "%s", knot_strerror(ret));
+		IXFROUT_LOG(LOG_ERR, "%s", knot_strerror(ret));
 		ret = NS_PROC_FAIL;
 		break;
 	}
@@ -339,90 +624,42 @@ int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata)
 	return ret;
 }
 
-int ixfr_process_answer(knot_pkt_t *pkt, struct answer_data *data)
+int ixfr_process_answer(knot_pkt_t *pkt, struct answer_data *adata)
 {
-#warning TODO: reimplement ixfr_process_answer
-#if 0
-	dbg_ns("ns_process_ixfrin: incoming packet\n");
-
-	/*
-	 * [TSIG] Here we assume that 'xfr' contains TSIG information
-	 * and the digest of the query sent to the master or the previous
-	 * digest.
-	 */
-	int ret = xfrin_process_ixfr_packet(pkt, xfr);
-
-	if (ret == XFRIN_RES_FALLBACK) {
-		dbg_ns("ns_process_ixfrin: Fallback to AXFR.\n");
-		ret = KNOT_ENOIXFR;
-	}
-
-	if (ret < 0) {
-		knot_pkt_free(&xfr->query);
-		return ret;
-	} else if (ret > 0) {
-		dbg_ns("ns_process_ixfrin: IXFR finished\n");
-		gettimeofday(&xfr->t_end, NULL);
-
-		knot_changesets_t *chgsets = (knot_changesets_t *)xfr->data;
-		if (chgsets == NULL || chgsets->first_soa == NULL) {
-			// nothing to be done??
-			dbg_ns("No changesets created for incoming IXFR!\n");
-			return ret;
+	if (adata->ext == NULL) {
+		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
+		if (!zone_transfer_needed(adata->param->zone, pkt)) {
+			IXFRIN_LOG(LOG_INFO, "Server has newer zone.");
+			return NS_PROC_DONE;
 		}
 
-		// find zone associated with the changesets
-		/* Must not search for the zone in zonedb as it may fetch a
-		 * different zone than the one the transfer started on. */
-		zone_t *zone = xfr->zone;
-		if (zone == NULL) {
-			dbg_ns("No zone found for incoming IXFR!\n");
-			knot_changesets_free(
-				(knot_changesets_t **)(&xfr->data));
-			return KNOT_ENOZONE;
+		IXFRIN_LOG(LOG_INFO, "Starting.");
+		// First packet with IXFR, init context
+		int ret = ixfrin_answer_init(adata);
+		if (ret != KNOT_EOK) {
+			IXFRIN_LOG(LOG_ERR, "Failed - %s", knot_strerror(ret));
+			return NS_PROC_FAIL;
 		}
+	} else {
+		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 100);
+	}
 
-		switch (ret) {
-		case XFRIN_RES_COMPLETE:
-			break;
-		case XFRIN_RES_SOA_ONLY: {
-			// compare the SERIAL from the changeset with the zone's
-			// serial
-			uint32_t zone_serial = zone_contents_serial(zone->contents);
-			if (knot_serial_compare(
-			      knot_soa_serial(&chgsets->first_soa->rrs),
-			      zone_serial)
-			    > 0) {
-				if ((xfr->flags & XFR_FLAG_UDP) != 0) {
-					// IXFR over UDP
-					dbg_ns("Update did not fit.\n");
-					return KNOT_EIXFRSPACE;
-				} else {
-					// fallback to AXFR
-					dbg_ns("ns_process_ixfrin: "
-					       "Fallback to AXFR.\n");
-					knot_changesets_free(
-					      (knot_changesets_t **)&xfr->data);
-					knot_pkt_free(&xfr->query);
-					return KNOT_ENOIXFR;
-				}
-
-			} else {
-				// free changesets
-				dbg_ns("No update needed.\n");
-				knot_changesets_free(
-					(knot_changesets_t **)(&xfr->data));
-				return KNOT_ENOXFR;
-			}
-		} break;
+	int ret = process_ixfrin_packet(pkt, adata);
+	if (ret == NS_PROC_DONE) {
+		NS_NEED_TSIG_SIGNED(&adata->param->tsig_ctx, 0);
+		int fret = ixfrin_finalize(adata);
+		if (fret != KNOT_EOK) {
+			IXFRIN_LOG(LOG_ERR, "Failed - %s", knot_strerror(ret));
+			ret = NS_PROC_FAIL;
 		}
 	}
 
-	/*! \todo In case of error, shouldn't the zone be destroyed here? */
+	if (ret == NS_PROC_FAIL) {
+		ixfrin_cleanup(adata);
+		IXFRIN_LOG(LOG_ERR, "Failed.");
+	}
 
 	return ret;
-#endif
-	return NS_PROC_FAIL;
 }
 
-#undef IXFR_LOG
+#undef IXFROUT_LOG
diff --git a/src/knot/nameserver/ixfr.h b/src/knot/nameserver/ixfr.h
index 1ee9130ac5f3a80eb6d4cfbe68abcdcc98a277a2..fcb0a9cb93db6dda7e81ece2986d95fe2945b468 100644
--- a/src/knot/nameserver/ixfr.h
+++ b/src/knot/nameserver/ixfr.h
@@ -39,32 +39,16 @@ struct answer_data;
  * \retval FAIL if it encountered an error.
  * \retval DONE if finished.
  */
-int ixfr_answer(knot_pkt_t *pkt, struct query_data *qdata);
+int ixfr_query(knot_pkt_t *pkt, struct query_data *qdata);
 
 /*!
- * \brief Process an IXFR query response.
- *
- * \param pkt Processed packet.
- * \param xfr Persistent transfer-specific data.
- *
- * \retval KNOT_EOK If this packet was processed successfuly and another packet
- *                  is expected. (RFC1995bis, case c)
- * \retval KNOT_ENOXFR If the transfer is not taking place because server's
- *                     SERIAL is the same as this client's SERIAL. The client
- *                     should close the connection and do no further processing.
- *                     (RFC1995bis case a).
- * \retval KNOT_EAGAIN If the server could not fit the transfer into the packet.
- *                     This should happen only if UDP was used. In this case
- *                     the client should retry the request via TCP. If UDP was
- *                     not used, it should be considered that the transfer was
- *                     malformed and the connection should be closed.
- *                     (RFC1995bis case b).
- * \retval >0 Transfer successully finished. Changesets are created and furter
- *            processing is needed.
- * \retval Other If any other error occured. The connection should be closed.
+ * \brief IXFR query response processing module.
  *
+ * \retval MORE if more data are required.
+ * \retval FAIL if it encountered an error, retry over AXFR will be done.
+ * \retval DONE if finished.
  */
-int ixfr_process_answer(knot_pkt_t *pkt, struct answer_data *data);
+int ixfr_process_answer(knot_pkt_t *pkt, struct answer_data *adata);
 
 #endif /* _KNOT_IXFR_H_ */
 
diff --git a/src/knot/nameserver/process_query.c b/src/knot/nameserver/process_query.c
index 7c28186c6d196fc2334636607432d980d6ff7db1..885e0b4c5dfdb163501f0d4f93c4f20b63294ad4 100644
--- a/src/knot/nameserver/process_query.c
+++ b/src/knot/nameserver/process_query.c
@@ -372,7 +372,7 @@ static int query_internet(knot_pkt_t *pkt, knot_process_t *ctx)
 		next_state = axfr_query(pkt, data);
 		break;
 	case KNOT_QUERY_IXFR:
-		next_state = ixfr_answer(pkt, data);
+		next_state = ixfr_query(pkt, data);
 		break;
 	case KNOT_QUERY_UPDATE:
 		next_state = update_answer(pkt, data);
diff --git a/src/knot/nameserver/update.c b/src/knot/nameserver/update.c
index 99f1d71721f92b0c919031d12f493eeba127d53b..a32005c3977b4bf0bd9a09ab57790373b20faf82 100644
--- a/src/knot/nameserver/update.c
+++ b/src/knot/nameserver/update.c
@@ -1,7 +1,7 @@
 #include "knot/nameserver/update.h"
 #include "knot/nameserver/internet.h"
 #include "knot/nameserver/process_query.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "knot/dnssec/zone-sign.h"
 #include "common/debug.h"
 #include "knot/dnssec/zone-events.h"
@@ -135,13 +135,13 @@ static bool zones_nsec3param_changed(const zone_contents_t *old_contents,
 }
 
 static int sign_update(zone_t *zone, const zone_contents_t *old_contents,
-                       zone_contents_t *new_contents, knot_changeset_t *ddns_ch)
+                       zone_contents_t *new_contents, changeset_t *ddns_ch)
 {
-	knot_changesets_t *sec_chs = knot_changesets_create(1);
+	changesets_t *sec_chs = changesets_create(1);
 	if (sec_chs == NULL) {
 		return KNOT_ENOMEM;
 	}
-	knot_changeset_t *sec_ch = knot_changesets_get_last(sec_chs);
+	changeset_t *sec_ch = changesets_get_last(sec_chs);
 
 	/*
 	 * Check if the UPDATE changed DNSKEYs or NSEC3PARAM.
@@ -162,22 +162,21 @@ static int sign_update(zone_t *zone, const zone_contents_t *old_contents,
 		                                 &refresh_at);
 	}
 	if (ret != KNOT_EOK) {
-		knot_changesets_free(&sec_chs);
+		changesets_free(&sec_chs, NULL);
 		return ret;
 	}
 
 	// Apply DNSSEC changeset
-	zone_contents_set_gen_old(new_contents);
-	ret = xfrin_apply_changesets_directly(new_contents, sec_chs);
+	ret = apply_changesets_directly(new_contents, sec_chs);
 	if (ret != KNOT_EOK) {
-		knot_changesets_free(&sec_chs);
+		changesets_free(&sec_chs, NULL);
 		return ret;
 	}
 
 	// Merge changesets
-	ret = knot_changeset_merge(ddns_ch, sec_ch);
+	ret = changeset_merge(ddns_ch, sec_ch);
 	if (ret != KNOT_EOK) {
-		knot_changesets_free(&sec_chs);
+		changesets_free(&sec_chs, NULL);
 		return ret;
 	}
 
@@ -203,33 +202,33 @@ static int process_authenticated(uint16_t *rcode, struct query_data *qdata)
 	}
 
 	// Create DDNS changesets
-	knot_changesets_t *ddns_chs = knot_changesets_create(1);
+	changesets_t *ddns_chs = changesets_create(1);
 	if (ddns_chs == NULL) {
 		*rcode = KNOT_RCODE_SERVFAIL;
 		return KNOT_ENOMEM;
 	}
-	knot_changeset_t *ddns_ch = knot_changesets_get_last(ddns_chs);
+	changeset_t *ddns_ch = changesets_get_last(ddns_chs);
 	ret = ddns_process_update(zone, query, ddns_ch, rcode);
 	if (ret != KNOT_EOK) {
-		knot_changesets_free(&ddns_chs);
+		changesets_free(&ddns_chs, NULL);
 		return ret;
 	}
 
 	zone_contents_t *new_contents = NULL;
-	const bool change_made = !knot_changeset_is_empty(ddns_ch);
+	const bool change_made = !changeset_is_empty(ddns_ch);
 	if (change_made) {
-		ret = xfrin_apply_changesets(zone, ddns_chs, &new_contents);
+		ret = apply_changesets(zone, ddns_chs, &new_contents);
 		if (ret != KNOT_EOK) {
 			if (ret == KNOT_ETTL) {
 				*rcode = KNOT_RCODE_REFUSED;
 			} else {
 				*rcode = KNOT_RCODE_SERVFAIL;
 			}
-			knot_changesets_free(&ddns_chs);
+			changesets_free(&ddns_chs, NULL);
 			return ret;
 		}
 	} else {
-		knot_changesets_free(&ddns_chs);
+		changesets_free(&ddns_chs, NULL);
 		*rcode = KNOT_RCODE_NOERROR;
 		return KNOT_EOK;
 	}
@@ -238,8 +237,8 @@ static int process_authenticated(uint16_t *rcode, struct query_data *qdata)
 	if (zone->conf->dnssec_enable) {
 		ret = sign_update(zone, zone->contents, new_contents, ddns_ch);
 		if (ret != KNOT_EOK) {
-			xfrin_rollback_update(ddns_chs, &new_contents);
-			knot_changesets_free(&ddns_chs);
+			update_rollback(ddns_chs, &new_contents);
+			changesets_free(&ddns_chs, NULL);
 			*rcode = KNOT_RCODE_SERVFAIL;
 			return ret;
 		}
@@ -248,20 +247,19 @@ static int process_authenticated(uint16_t *rcode, struct query_data *qdata)
 	// Write changes to journal if all went well. (DNSSEC merged)
 	ret = zone_change_store(zone, ddns_chs);
 	if (ret != KNOT_EOK) {
-		xfrin_rollback_update(ddns_chs, &new_contents);
-		knot_changesets_free(&ddns_chs);
+		update_rollback(ddns_chs, &new_contents);
+		changesets_free(&ddns_chs, NULL);
 		*rcode = KNOT_RCODE_SERVFAIL;
 		return ret;
 	}
 
 	// Switch zone contents.
-	zone_contents_t *old_contents = xfrin_switch_zone(zone, new_contents);
-	// Wait for readers.
+	zone_contents_t *old_contents = zone_switch_contents(zone, new_contents);
 	synchronize_rcu();
-	xfrin_zone_contents_free(&old_contents);
+	update_free_old_zone(&old_contents);
 
-	xfrin_cleanup_successful_update(ddns_chs);
-	knot_changesets_free(&ddns_chs);
+	update_cleanup(ddns_chs);
+	changesets_free(&ddns_chs, NULL);
 
 	/* Trim extra heap. */
 	mem_trim();
diff --git a/src/knot/server/journal.c b/src/knot/server/journal.c
index 3e5f72c2d9131b9f294b023e6e122e529cb0d800..eb629f6bfccec9cfd5dcf39ea35aa521bbb18a8b 100644
--- a/src/knot/server/journal.c
+++ b/src/knot/server/journal.c
@@ -897,7 +897,15 @@ int journal_unmap(journal_t *journal, uint64_t id, void *ptr, int finalize)
 	}
 
 	/* Mapped node is on tail. */
+	/* @todo: This is hack to allow read-only correct unmap. */
+	int ret = KNOT_EOK;
 	journal_node_t *n = journal->nodes + journal->qtail;
+	if (!finalize) {
+		ret = journal_fetch(journal, id, journal_cmp_eq, &n);
+		if (ret != KNOT_EOK) {
+			return KNOT_ENOENT;
+		}
+	}
 	if(n->id != id) {
 		dbg_journal("journal: failed to find mmap node with id=%llu\n",
 		            (unsigned long long)id);
@@ -917,7 +925,6 @@ int journal_unmap(journal_t *journal, uint64_t id, void *ptr, int finalize)
 	}
 
 	/* Finalize. */
-	int ret = KNOT_EOK;
 	if (finalize) {
 		ret = journal_write_out(journal, n);
 	}
@@ -953,7 +960,7 @@ bool journal_exists(const char *path)
 }
 
 /*! \brief No doc here. Moved from zones.h (@mvavrusa) */
-static int changesets_unpack(knot_changeset_t *chs)
+static int changesets_unpack(changeset_t *chs)
 {
 
 	knot_rrset_t *rrset = NULL;
@@ -963,8 +970,6 @@ static int changesets_unpack(knot_changeset_t *chs)
 		return KNOT_EMALF;
 	}
 	size_t remaining = chs->size;
-	memcpy(&chs->flags, chs->data, sizeof(uint32_t));
-	remaining -= sizeof(uint32_t);
 
 	/* Read initial changeset RRSet - SOA. */
 	uint8_t *stream = chs->data + (chs->size - remaining);
@@ -979,7 +984,7 @@ static int changesets_unpack(knot_changeset_t *chs)
 			 */
 	assert(rrset->type == KNOT_RRTYPE_SOA);
 	assert(chs->serial_from == knot_soa_serial(&rrset->rrs));
-	knot_changeset_add_soa(chs, rrset, KNOT_CHANGESET_REMOVE);
+	changeset_add_soa(chs, rrset, CHANGESET_REMOVE);
 
 	/* Read remaining RRSets */
 	int in_remove_section = 1;
@@ -998,8 +1003,8 @@ static int changesets_unpack(knot_changeset_t *chs)
 
 			/* Move to ADD section if in REMOVE. */
 			if (in_remove_section) {
-				knot_changeset_add_soa(chs, rrset,
-				                       KNOT_CHANGESET_ADD);
+				changeset_add_soa(chs, rrset,
+				                       CHANGESET_ADD);
 				in_remove_section = 0;
 			} else {
 				/* Final SOA. */
@@ -1009,14 +1014,14 @@ static int changesets_unpack(knot_changeset_t *chs)
 		} else {
 			/* Remove RRSets. */
 			if (in_remove_section) {
-				ret = knot_changeset_add_rrset(
+				ret = changeset_add_rrset(
 				              chs, rrset,
-				              KNOT_CHANGESET_REMOVE);
+				              CHANGESET_REMOVE);
 			} else {
 				/* Add RRSets. */
-				ret = knot_changeset_add_rrset(
+				ret = changeset_add_rrset(
 				              chs, rrset,
-				              KNOT_CHANGESET_ADD);
+				              CHANGESET_ADD);
 			}
 		}
 	}
@@ -1038,14 +1043,9 @@ static int zones_rrset_write_to_mem(const knot_rrset_t *rr, char **entry,
 	return ret;
 }
 
-static int zones_serialize_and_store_chgset(const knot_changeset_t *chs,
+static int zones_serialize_and_store_chgset(const changeset_t *chs,
                                             char *entry, size_t max_size)
 {
-	/* Write changeset flags. */
-	memcpy(entry, (char*)&chs->flags, sizeof(uint32_t));
-	entry += sizeof(uint32_t);
-	max_size -= sizeof(uint32_t);
-
 	/* Serialize SOA 'from'. */
 	int ret = zones_rrset_write_to_mem(chs->soa_from, &entry, &max_size);
 	if (ret != KNOT_EOK) {
@@ -1085,7 +1085,7 @@ static int zones_serialize_and_store_chgset(const knot_changeset_t *chs,
 	return KNOT_EOK;
 }
 
-static int changeset_pack(const knot_changeset_t *chs, journal_t *j)
+static int changeset_pack(const changeset_t *chs, journal_t *j)
 {
 	assert(chs != NULL);
 	assert(j != NULL);
@@ -1174,8 +1174,8 @@ finish:
 
 static int load_changeset(journal_t *journal, journal_node_t *n, void *data)
 {
-	knot_changesets_t *chsets = (knot_changesets_t *)data;
-	knot_changeset_t *chs = knot_changesets_create_changeset(chsets);
+	changesets_t *chsets = (changesets_t *)data;
+	changeset_t *chs = changesets_create_changeset(chsets);
 	if (chs == NULL) {
 		return KNOT_ERROR;
 	}
@@ -1199,7 +1199,7 @@ static int load_changeset(journal_t *journal, journal_node_t *n, void *data)
 	return KNOT_EOK;
 }
 
-int journal_load_changesets(const char *path, knot_changesets_t *dst,
+int journal_load_changesets(const char *path, changesets_t *dst,
                             uint32_t from, uint32_t to)
 {
 	int ret = journal_walk(path, from, to, &load_changeset, dst);
@@ -1213,7 +1213,7 @@ int journal_load_changesets(const char *path, knot_changesets_t *dst,
 	 * Parses changesets from the binary format stored in chgsets->data
 	 * into the changeset_t structures.
 	 */
-	knot_changeset_t* chs = NULL;
+	changeset_t* chs = NULL;
 	WALK_LIST(chs, dst->sets) {
 		ret = changesets_unpack(chs);
 		if (ret != KNOT_EOK) {
@@ -1222,7 +1222,7 @@ int journal_load_changesets(const char *path, knot_changesets_t *dst,
 	}
 
 	/* Check for complete history. */
-	knot_changeset_t *last = knot_changesets_get_last(dst);
+	changeset_t *last = changesets_get_last(dst);
 	if (to != last->serial_to) {
 		return KNOT_ERANGE;
 	}
@@ -1230,7 +1230,7 @@ int journal_load_changesets(const char *path, knot_changesets_t *dst,
 	return KNOT_EOK;
 }
 
-int journal_store_changesets(knot_changesets_t *src, const char *path, size_t size_limit)
+int journal_store_changesets(changesets_t *src, const char *path, size_t size_limit)
 {
 	if (src == NULL || path == NULL) {
 		return KNOT_EINVAL;
@@ -1244,7 +1244,7 @@ int journal_store_changesets(knot_changesets_t *src, const char *path, size_t si
 	}
 
 	/* Begin writing to journal. */
-	knot_changeset_t *chs = NULL;
+	changeset_t *chs = NULL;
 	WALK_LIST(chs, src->sets) {
 		/* Make key from serials. */
 		ret = changeset_pack(chs, journal);
diff --git a/src/knot/server/journal.h b/src/knot/server/journal.h
index f0c43be0daf3d59c50baa936690979c5eedf1909..21da6a50776a478687b07da0797b360ff5c147e6 100644
--- a/src/knot/server/journal.h
+++ b/src/knot/server/journal.h
@@ -102,7 +102,7 @@ typedef struct journal_t
  * Journal defaults and constants.
  */
 #define JOURNAL_NCOUNT 1024 /*!< Default node count. */
-#define JOURNAL_MAGIC {'k', 'n', 'o', 't', '1', '5', '1'}
+#define JOURNAL_MAGIC {'k', 'n', 'o', 't', '1', '5', '2'}
 #define MAGIC_LENGTH 7
 /* HEADER = magic, crc, max_entries, qhead, qtail */
 #define JOURNAL_HSIZE (MAGIC_LENGTH + sizeof(crc_t) + sizeof(uint16_t) * 3)
@@ -173,7 +173,7 @@ bool journal_exists(const char *path);
  *
  * \param changesets Double pointer to changesets structure to be freed.
  */
-int journal_load_changesets(const char *path, knot_changesets_t *dst,
+int journal_load_changesets(const char *path, changesets_t *dst,
                             uint32_t from, uint32_t to);
 
 /*!
@@ -192,7 +192,7 @@ int journal_load_changesets(const char *path, knot_changesets_t *dst,
  * \todo Expects the xfr structure to be initialized in some way.
  * \todo Update documentation!!!
  */
-int journal_store_changesets(knot_changesets_t *src,  const char *path, size_t size_limit);
+int journal_store_changesets(changesets_t *src,  const char *path, size_t size_limit);
 
 /*! \brief Function for unmarking dirty nodes. */
 int journal_mark_synced(const char *path);
diff --git a/src/knot/server/net.c b/src/knot/server/net.c
index 38e1e6f0b5bd05a41511cae1118752bfa73c6097..e0a010dea4f2ded2c839d02eff90dc5163addbb8 100644
--- a/src/knot/server/net.c
+++ b/src/knot/server/net.c
@@ -114,6 +114,11 @@ int net_connected_socket(int type, const struct sockaddr_storage *dst_addr,
 
 	int socket = -1;
 
+	/* Check port. */
+	if (sockaddr_port(dst_addr) == 0) {
+		return KNOT_ECONN;
+	}
+
 	/* Bind to specific source address - if set. */
 	if (src_addr != NULL && src_addr->ss_family != AF_UNSPEC) {
 		socket = net_bound_socket(type, src_addr);
diff --git a/src/knot/server/serialization.c b/src/knot/server/serialization.c
index f79ec458bddd409ab50ba04c3da6603c37b2c1e0..21c0d9d68c9fb7b951704abdaf4c02af6d4cde4e 100644
--- a/src/knot/server/serialization.c
+++ b/src/knot/server/serialization.c
@@ -69,7 +69,7 @@ static int deserialize_rr(knot_rrset_t *rrset, const uint8_t *stream, uint32_t r
 	                         rdata_size - sizeof(uint32_t), ttl, NULL);
 }
 
-int changeset_binary_size(const knot_changeset_t *chgset, size_t *size)
+int changeset_binary_size(const changeset_t *chgset, size_t *size)
 {
 	if (chgset == NULL || size == NULL) {
 		return KNOT_EINVAL;
diff --git a/src/knot/server/serialization.h b/src/knot/server/serialization.h
index cd0f9aa139f7b22bacddc08a90679c625a7f1e3c..1fa37ddddb5182e3b55d0960d6362f2fa12d45d2 100644
--- a/src/knot/server/serialization.h
+++ b/src/knot/server/serialization.h
@@ -38,7 +38,7 @@
  *
  * \return KNOT_E*
  */
-int changeset_binary_size(const knot_changeset_t *chgset, size_t *size);
+int changeset_binary_size(const changeset_t *chgset, size_t *size);
 
 /*!
  * \brief Serializes one RRSet into given stream.
diff --git a/src/knot/server/server.c b/src/knot/server/server.c
index 8a8e72f2c057b673bef98f60368b63c60873b319..3781abc35864cd3fafed55b023529e6c4099be8d 100644
--- a/src/knot/server/server.c
+++ b/src/knot/server/server.c
@@ -278,6 +278,7 @@ int server_init(server_t *server)
 	}
 
 	/* Create zone events threads. */
+#warning TODO: config option
 	server->workers = worker_pool_create(4); //! \todo config option
 	if (server->workers == NULL) {
 		dt_delete(&server->iosched);
diff --git a/src/knot/server/xfr-handler.c b/src/knot/server/xfr-handler.c
index 1ae51dba73eb3df163ef8a4099a6e3d5c6980cb3..0d5fa1eee2e783ed64823de479d8342458226bf5 100644
--- a/src/knot/server/xfr-handler.c
+++ b/src/knot/server/xfr-handler.c
@@ -32,7 +32,7 @@
 #include "knot/server/net.h"
 #include "knot/server/udp-handler.h"
 #include "knot/server/tcp-handler.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "knot/nameserver/axfr.h"
 #include "knot/nameserver/ixfr.h"
 #include "knot/nameserver/internet.h"
@@ -255,8 +255,8 @@ static void xfr_task_cleanup(knot_ns_xfr_t *rq)
 			rq->data = NULL;
 		}
 	} else if (rq->type == XFR_TYPE_IIN) {
-		knot_changesets_t *chs = (knot_changesets_t *)rq->data;
-		knot_changesets_free(&chs);
+		changesets_t *chs = (changesets_t *)rq->data;
+		changesets_free(&chs);
 		rq->data = NULL;
 		assert(rq->new_contents == NULL);
 	} else if (rq->type == XFR_TYPE_FORWARD) {
@@ -590,7 +590,7 @@ static int xfr_task_finalize(knot_ns_xfr_t *rq)
 		ret = zones_save_zone(rq);
 		if (ret == KNOT_EOK) {
 			zone_contents_t *old_contents = NULL;
-			old_contents = xfrin_switch_zone(rq->zone, rq->new_contents);
+			old_contents = update_switch_contents(rq->zone, rq->new_contents);
 			zone_contents_deep_free(&old_contents);
 		} else {
 			log_zone_error("%s %s\n",
@@ -599,8 +599,8 @@ static int xfr_task_finalize(knot_ns_xfr_t *rq)
 			               rq->msg);
 		}
 	} else if (rq->type == XFR_TYPE_IIN) {
-		knot_changesets_t *chs = (knot_changesets_t *)rq->data;
-		ret = zone_change_apply_and_store(chs, rq->zone,
+		changesets_t *chs = (changesets_t *)rq->data;
+		ret = zone_change_apply_and_store(&chs, rq->zone,
 		                                  &rq->new_contents,
 		                                  rq->msg);
 		rq->data = NULL; /* Freed or applied in prev function. */
@@ -862,7 +862,7 @@ static int xfr_task_xfer(xfrhandler_t *xfr, knot_ns_xfr_t *rq, knot_pkt_t *pkt)
 		ret = axfr_process_answer(pkt, rq);
 		break;
 	case XFR_TYPE_IIN:
-		ret = ixfr_process_answer(pkt, rq);
+		ret = ixfrin_process_answer(pkt, rq);
 		break;
 	default:
 		ret = KNOT_EINVAL;
diff --git a/src/knot/updates/apply.c b/src/knot/updates/apply.c
new file mode 100644
index 0000000000000000000000000000000000000000..77d3d1c138cbcd508d136f3c9898360c0609b1ea
--- /dev/null
+++ b/src/knot/updates/apply.c
@@ -0,0 +1,723 @@
+/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <assert.h>
+
+#include "knot/updates/apply.h"
+
+#include "common/debug.h"
+#include "libknot/packet/pkt.h"
+#include "libknot/processing/process.h"
+#include "libknot/dname.h"
+#include "knot/zone/zone.h"
+#include "libknot/common.h"
+#include "knot/updates/changesets.h"
+#include "knot/zone/zonefile.h"
+#include "common/lists.h"
+#include "common/descriptor.h"
+#include "libknot/util/utils.h"
+#include "libknot/rdata/soa.h"
+
+/* ------------------------ legacy, to be removed --------------------------- */
+
+#define KNOT_NS_TSIG_FREQ 100
+
+static int knot_ns_tsig_required(int packet_nr)
+{
+	/*! \bug This can overflow to negative numbers. Proper solution is to
+	 *       count exactly at one place for each incoming/outgoing packet
+	 *       with packet_nr = (packet_nr + 1) % FREQ and require TSIG on 0.
+	 */
+	dbg_ns_verb("ns_tsig_required(%d): %d\n", packet_nr,
+	            (packet_nr % KNOT_NS_TSIG_FREQ == 0));
+	return (packet_nr % KNOT_NS_TSIG_FREQ == 0);
+}
+
+int xfrin_transfer_needed(const zone_contents_t *zone,
+                          knot_pkt_t *soa_response)
+{
+#warning Reimplement or remove
+	/*
+	 * Retrieve the local Serial
+	 */
+	const knot_rdataset_t *soa_rrs =
+		node_rdataset(zone->apex, KNOT_RRTYPE_SOA);
+	if (soa_rrs == NULL) {
+		char *name = knot_dname_to_str(zone->apex->owner);
+		dbg_xfrin("SOA RRSet missing in the zone %s!\n", name);
+		free(name);
+		return KNOT_ERROR;
+	}
+
+	uint32_t local_serial = knot_soa_serial(soa_rrs);
+	/*
+	 * Retrieve the remote Serial
+	 */
+	// the SOA should be the first (and only) RRSet in the response
+	const knot_pktsection_t *answer = knot_pkt_section(soa_response, KNOT_ANSWER);
+	if (answer->count < 1) {
+		return KNOT_EMALF;
+	}
+	knot_rrset_t soa_rr = answer->rr[0];
+	if (soa_rr.type != KNOT_RRTYPE_SOA) {
+		return KNOT_EMALF;
+	}
+
+	uint32_t remote_serial = knot_soa_serial(&soa_rr.rrs);
+	return (knot_serial_compare(local_serial, remote_serial) < 0);
+}
+
+static int xfrin_check_tsig(knot_pkt_t *packet, knot_ns_xfr_t *xfr,
+                            int tsig_req)
+{
+#warning reimplement, but not inside this file
+	assert(packet != NULL);
+	assert(xfr != NULL);
+
+	dbg_xfrin_verb("xfrin_check_tsig(): packet nr: %d, required: %d\n",
+		       xfr->packet_nr, tsig_req);
+
+	/*
+	 * If we are expecting it (i.e. xfr->prev_digest_size > 0)
+	 *   a) it should be there (first, last or each 100th packet) and it
+	 *      is not
+	 *        Then we should discard the changes and close the connection.
+	 *   b) it should be there and it is or it may not be there (other
+	 *      packets) and it is
+	 *        We validate the TSIG and reset packet number counting and
+	 *        data aggregation.
+	 *
+	 * If we are not expecting it (i.e. xfr->prev_digest_size <= 0) and
+	 * it is there => it should probably be considered an error
+	 */
+
+	int ret = KNOT_EOK;
+	if (xfr->tsig_key) {
+		// just append the wireformat to the TSIG data
+		uint8_t *wire_buf = xfr->tsig_data + xfr->tsig_data_size;
+		memcpy(wire_buf, packet->wire, packet->size);
+		xfr->tsig_data_size += packet->size;
+	}
+
+	if (xfr->tsig_key) {
+		if (tsig_req && packet->tsig_rr == NULL) {
+			// TSIG missing!!
+			return KNOT_ENOTSIG;
+		} else if (packet->tsig_rr != NULL) {
+			// TSIG there, either required or not, process
+			if (xfr->packet_nr == 0) {
+				ret = knot_tsig_client_check(packet->tsig_rr,
+					xfr->tsig_data, xfr->tsig_data_size,
+					xfr->digest, xfr->digest_size,
+					xfr->tsig_key,
+					xfr->tsig_prev_time_signed);
+			} else {
+				ret = knot_tsig_client_check_next(packet->tsig_rr,
+					xfr->tsig_data, xfr->tsig_data_size,
+					xfr->digest, xfr->digest_size,
+					xfr->tsig_key,
+					xfr->tsig_prev_time_signed);
+			}
+
+			if (ret != KNOT_EOK) {
+				/* No need to check TSIG error
+				 * here, propagate and check elsewhere.*/
+				return ret;
+			}
+
+			// and reset the data storage
+			//xfr->packet_nr = 1;
+			xfr->tsig_data_size = 0;
+
+			// Extract the digest from the TSIG RDATA and store it.
+			if (xfr->digest_max_size < tsig_rdata_mac_length(packet->tsig_rr)) {
+				return KNOT_ESPACE;
+			}
+			memcpy(xfr->digest, tsig_rdata_mac(packet->tsig_rr),
+			       tsig_rdata_mac_length(packet->tsig_rr));
+			xfr->digest_size = tsig_rdata_mac_length(packet->tsig_rr);
+
+			// Extract the time signed from the TSIG and store it
+			// We may rewrite the tsig_req_time_signed field
+			xfr->tsig_prev_time_signed =
+					tsig_rdata_time_signed(packet->tsig_rr);
+
+		}
+	} else if (packet->tsig_rr != NULL) {
+		// TSIG where it should not be
+		return KNOT_EMALF;
+	}
+
+	return KNOT_EOK;
+}
+
+/* --------------------------- Update cleanup ------------------------------- */
+
+/*!
+ * \brief Post update cleanup: frees data that are in the tree that will not
+ *        be used (old tree if success, new tree if failure).
+ *          Freed data:
+ *           - actual data inside knot_rrs_t. (the rest is part of the node)
+ */
+static void rrs_list_clear(list_t *l, mm_ctx_t *mm)
+{
+	ptrnode_t *n;
+	node_t *nxt;
+	WALK_LIST_DELSAFE(n, nxt, *l) {
+		mm_free(mm, (void *)n->d);
+		mm_free(mm, n);
+	};
+}
+
+/*! \brief Frees additional data from single node */
+static int free_additional(zone_node_t **node, void *data)
+{
+#warning - TODO: still needed?
+	UNUSED(data);
+	if ((*node)->flags & NODE_FLAGS_NONAUTH) {
+		// non-auth nodes have no additionals.
+		return KNOT_EOK;
+	}
+
+	for (uint16_t i = 0; i < (*node)->rrset_count; ++i) {
+		struct rr_data *data = &(*node)->rrs[i];
+		if (data->additional) {
+			free(data->additional);
+			data->additional = NULL;
+		}
+	}
+
+	return KNOT_EOK;
+}
+
+/* -------------------- Changeset application helpers ----------------------- */
+
+/*! \brief Replaces rdataset of given type with a copy. */
+static int replace_rdataset_with_copy(zone_node_t *node, uint16_t type)
+{
+	// Find data to copy.
+	struct rr_data *data = NULL;
+	for (uint16_t i = 0; i < node->rrset_count; ++i) {
+		if (node->rrs[i].type == type) {
+			data = &node->rrs[i];
+			break;
+		}
+	}
+	assert(data);
+
+	// Create new data.
+	knot_rdataset_t *rrs = &data->rrs;
+	void *copy = malloc(knot_rdataset_size(rrs));
+	if (copy == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	memcpy(copy, rrs->data, knot_rdataset_size(rrs));
+
+	// Store new data into node RRS.
+	rrs->data = copy;
+
+	return KNOT_EOK;
+}
+
+/*! \brief Stores RR data for update cleanup. */
+static void clear_new_rrs(zone_node_t *node, uint16_t type)
+{
+	knot_rdataset_t *new_rrs = node_rdataset(node, type);
+	if (new_rrs) {
+		knot_rdataset_clear(new_rrs, NULL);
+	}
+}
+
+/*! \brief Stores RR data for update cleanup. */
+static int add_old_data(changeset_t *chset, knot_rdata_t *old_data)
+{
+	if (ptrlist_add(&chset->old_data, old_data, NULL) == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Stores RR data for update rollback. */
+static int add_new_data(changeset_t *chset, knot_rdata_t *new_data)
+{
+	if (ptrlist_add(&chset->new_data, new_data, NULL) == NULL) {
+		return KNOT_ENOMEM;
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Returns true if given RR is present in node and can be removed. */
+static bool can_remove(const zone_node_t *node, const knot_rrset_t *rr)
+{
+	if (node == NULL) {
+		// Node does not exist, cannot remove anything.
+		return false;
+	}
+	const knot_rdataset_t *node_rrs = node_rdataset(node, rr->type);
+	if (node_rrs == NULL) {
+		// Node does not have this type at all.
+		return false;
+	}
+
+	const bool compare_ttls = false;
+	for (uint16_t i = 0; i < rr->rrs.rr_count; ++i) {
+		knot_rdata_t *rr_cmp = knot_rdataset_at(&rr->rrs, i);
+		if (knot_rdataset_member(node_rrs, rr_cmp, compare_ttls)) {
+			// At least one RR matches.
+			return true;
+		}
+	}
+
+	// Node does have the type, but no RRs match.
+	return false;
+}
+
+/*! \brief Removes single RR from zone contents. */
+static int remove_rr(zone_node_t *node, const knot_rrset_t *rr,
+                     changeset_t *chset)
+{
+	knot_rrset_t removed_rrset = node_rrset(node, rr->type);
+	knot_rdata_t *old_data = removed_rrset.rrs.data;
+	int ret = replace_rdataset_with_copy(node, rr->type);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	// Store old data for cleanup.
+	ret = add_old_data(chset, old_data);
+	if (ret != KNOT_EOK) {
+		clear_new_rrs(node, rr->type);
+		return ret;
+	}
+
+	knot_rdataset_t *changed_rrs = node_rdataset(node, rr->type);
+	// Subtract changeset RRS from node RRS.
+	ret = knot_rdataset_subtract(changed_rrs, &rr->rrs, NULL);
+	if (ret != KNOT_EOK) {
+		clear_new_rrs(node, rr->type);
+		return ret;
+	}
+
+	if (changed_rrs->rr_count > 0) {
+		// Subtraction left some data in RRSet, store it for rollback.
+		ret = add_new_data(chset, changed_rrs->data);
+		if (ret != KNOT_EOK) {
+			knot_rdataset_clear(changed_rrs, NULL);
+			return ret;
+		}
+	} else {
+		// RRSet is empty now, remove it from node, all data freed.
+		node_remove_rdataset(node, rr->type);
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Removes all RRs from changeset from zone contents. */
+static int apply_remove(zone_contents_t *contents, changeset_t *chset)
+{
+	knot_rr_ln_t *rr_node = NULL;
+	WALK_LIST(rr_node, chset->remove) {
+		const knot_rrset_t *rr = rr_node->rr;
+
+		// Find node for this owner
+		zone_node_t *node = zone_contents_find_node_for_rr(contents,
+		                                                   rr);
+		if (!can_remove(node, rr)) {
+			// Nothing to remove from, skip.
+			continue;
+		}
+
+		int ret = remove_rr(node, rr, chset);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Adds a single RR into zone contents. */
+static int add_rr(zone_node_t *node, const knot_rrset_t *rr,
+                  changeset_t *chset, bool master)
+{
+	knot_rrset_t changed_rrset = node_rrset(node, rr->type);
+	if (!knot_rrset_empty(&changed_rrset)) {
+		// Modifying existing RRSet.
+		knot_rdata_t *old_data = changed_rrset.rrs.data;
+		int ret = replace_rdataset_with_copy(node, rr->type);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+
+		// Store old RRS for cleanup.
+		ret = add_old_data(chset, old_data);
+		if (ret != KNOT_EOK) {
+			clear_new_rrs(node, rr->type);
+			return ret;
+		}
+	}
+
+	// Insert new RR to RRSet, data will be copied.
+	int ret = node_add_rrset(node, rr);
+	if (ret == KNOT_EOK || ret == KNOT_ETTL) {
+		// RR added, store for possible rollback.
+		knot_rdataset_t *rrs = node_rdataset(node, rr->type);
+		int data_ret = add_new_data(chset, rrs->data);
+		if (data_ret != KNOT_EOK) {
+			knot_rdataset_clear(rrs, NULL);
+			return data_ret;
+		}
+
+		if (ret == KNOT_ETTL) {
+			// Handle possible TTL errors.
+			log_ttl_error(node, rr);
+			if (!master) {
+				// TTL errors fatal only for master.
+				return KNOT_EOK;
+			}
+		}
+	}
+
+	return ret;
+}
+
+/*! \brief Adds all RRs from changeset into zone contents. */
+static int apply_add(zone_contents_t *contents, changeset_t *chset,
+                     bool master)
+{
+	knot_rr_ln_t *rr_node = NULL;
+	WALK_LIST(rr_node, chset->add) {
+		knot_rrset_t *rr = rr_node->rr;
+
+		// Get or create node with this owner
+		zone_node_t *node = zone_contents_get_node_for_rr(contents, rr);
+		if (node == NULL) {
+			return KNOT_ENOMEM;
+		}
+
+		int ret = add_rr(node, rr, chset, master);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Replace old SOA with a new one. */
+static int apply_replace_soa(zone_contents_t *contents, changeset_t *chset)
+{
+	assert(chset->soa_from);
+	int ret = remove_rr(contents->apex, chset->soa_from, chset);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	assert(!node_rrtype_exists(contents->apex, KNOT_RRTYPE_SOA));
+
+	return add_rr(contents->apex, chset->soa_to, chset, false);
+}
+
+/*! \brief Apply single change to zone contents structure. */
+static int apply_changeset(zone_contents_t *contents, changeset_t *chset,
+                           bool master)
+{
+	/*
+	 * Applies one changeset to the zone. Checks if the changeset may be
+	 * applied (i.e. the origin SOA (soa_from) has the same serial as
+	 * SOA in the zone apex.
+	 */
+
+	dbg_xfrin("APPLYING CHANGESET: from serial %u to serial %u\n",
+		  chset->serial_from, chset->serial_to);
+
+	// check if serial matches
+	const knot_rdataset_t *soa = node_rdataset(contents->apex, KNOT_RRTYPE_SOA);
+	if (soa == NULL || knot_soa_serial(soa) != chset->serial_from) {
+		dbg_xfrin("SOA serials do not match!!\n");
+		return KNOT_EINVAL;
+	}
+
+	int ret = apply_remove(contents, chset);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	ret = apply_add(contents, chset, master);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	return apply_replace_soa(contents, chset);
+}
+
+/* ------------------------- Empty node cleanup ----------------------------- */
+
+/*! \brief Mark empty nodes in updated tree. */
+static int mark_empty(zone_node_t **node_p, void *data)
+{
+	assert(node_p && *node_p);
+	zone_node_t *node = *node_p;
+	list_t *l = (list_t *)data;
+	assert(data);
+	if (node->rrset_count == 0 && node->children == 0 &&
+	    !(node->flags & NODE_FLAGS_EMPTY)) {
+		/*!
+		 * Mark this node and all parent nodes that have 0 RRSets and
+		 * no children for removal.
+		 */
+		if (ptrlist_add(l, node, NULL) == NULL) {
+			return KNOT_ENOMEM;
+		}
+		node->flags |= NODE_FLAGS_EMPTY;
+		if (node->parent) {
+			if ((node->parent->flags & NODE_FLAGS_WILDCARD_CHILD)
+			    && knot_dname_is_wildcard(node->owner)) {
+				node->parent->flags &= ~NODE_FLAGS_WILDCARD_CHILD;
+			}
+			node->parent->children--;
+			// Recurse using the parent node
+			return mark_empty(&node->parent, data);
+		}
+	}
+
+	return KNOT_EOK;
+}
+
+/*! \brief Removes node that were previously marked as empty. */
+static int remove_empty_nodes(zone_contents_t *z)
+{
+	list_t l;
+	init_list(&l);
+	// walk through the zone and select nodes to be removed
+	int ret = knot_zone_tree_apply(z->nodes, mark_empty, &l);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	ptrnode_t *n = NULL;
+	node_t *nxt = NULL;
+	WALK_LIST_DELSAFE(n, nxt, l) {
+		zone_node_t *node = (zone_node_t *)n->d;
+		ret = zone_contents_remove_node(z, node->owner);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+		node_free(&node);
+		free(n);
+	}
+
+	init_list(&l);
+	// Do the same with NSEC3 nodes.
+	ret = knot_zone_tree_apply(z->nsec3_nodes, mark_empty, &l);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	WALK_LIST_DELSAFE(n, nxt, l) {
+		zone_node_t *node = (zone_node_t *)n->d;
+		ret = zone_contents_remove_nsec3_node(z, node->owner);
+		if (ret != KNOT_EOK) {
+			return ret;
+		}
+		node_free(&node);
+		free(n);
+	}
+
+	return KNOT_EOK;
+}
+
+/* --------------------- Zone copy and finalization ------------------------- */
+
+/*! \brief Creates a shallow zone contents copy. */
+static int prepare_zone_copy(zone_contents_t *old_contents,
+                             zone_contents_t **new_contents)
+{
+	if (old_contents == NULL || new_contents == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	/*
+	 * Create a shallow copy of the zone, so that the structures may be
+	 * updated.
+	 *
+	 * This will create new zone contents structures (normal nodes' tree,
+	 * NSEC3 tree), and copy all nodes.
+	 * The data in the nodes (RRSets) remain the same though.
+	 */
+	zone_contents_t *contents_copy = NULL;
+	int ret = zone_contents_shallow_copy(old_contents, &contents_copy);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	*new_contents = contents_copy;
+
+	return KNOT_EOK;
+}
+
+/*! \brief Removes empty nodes from updated zone a does zone adjusting. */
+static int finalize_updated_zone(zone_contents_t *contents_copy,
+                                 bool set_nsec3_names)
+{
+	if (contents_copy == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	int ret = remove_empty_nodes(contents_copy);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	if (set_nsec3_names) {
+		ret = zone_contents_adjust_full(contents_copy, NULL, NULL);
+	} else {
+		ret = zone_contents_adjust_pointers(contents_copy);
+	}
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	return KNOT_EOK;
+}
+
+/* ------------------------------- API -------------------------------------- */
+
+int apply_changesets(zone_t *zone, changesets_t *chsets,
+                     zone_contents_t **new_contents)
+{
+	if (zone == NULL || changesets_empty(chsets) || new_contents == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	zone_contents_t *old_contents = zone->contents;
+	if (!old_contents) {
+		return KNOT_EINVAL;
+	}
+
+	zone_contents_t *contents_copy = NULL;
+	int ret = prepare_zone_copy(old_contents, &contents_copy);
+	if (ret != KNOT_EOK) {
+		return ret;
+	}
+
+	/*
+	 * Apply the changesets.
+	 */
+	changeset_t *set = NULL;
+	const bool master = (zone_master(zone) == NULL);
+	WALK_LIST(set, chsets->sets) {
+		ret = apply_changeset(contents_copy, set, master);
+		if (ret != KNOT_EOK) {
+			update_rollback(chsets, &contents_copy);
+			return ret;
+		}
+	}
+
+	assert(contents_copy->apex != NULL);
+
+	ret = finalize_updated_zone(contents_copy, true);
+	if (ret != KNOT_EOK) {
+		update_rollback(chsets, &contents_copy);
+		return ret;
+	}
+
+	*new_contents = contents_copy;
+
+	return KNOT_EOK;
+}
+
+int apply_changesets_directly(zone_contents_t *contents, changesets_t *chsets)
+{
+	if (contents == NULL || chsets == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	changeset_t *set = NULL;
+	WALK_LIST(set, chsets->sets) {
+		const bool master = true; // Only DNSSEC changesets are applied directly.
+		int ret = apply_changeset(contents, set, master);
+		if (ret != KNOT_EOK) {
+			update_cleanup(chsets);
+			return ret;
+		}
+	}
+
+	int ret = finalize_updated_zone(contents, true);
+
+	/*
+	 * HACK: Cleanup for successful update is used for both success and fail
+	 * when modifying the zone directly, will fix in new zone API.
+	 */
+	update_cleanup(chsets);
+	return ret;
+}
+
+void update_cleanup(changesets_t *chgs)
+{
+	if (chgs == NULL) {
+		return;
+	}
+
+	changeset_t *change = NULL;
+	WALK_LIST(change, chgs->sets) {
+		// Delete old RR data
+		rrs_list_clear(&change->old_data, NULL);
+		init_list(&change->old_data);
+		// Keep new RR data
+		ptrlist_free(&change->new_data, NULL);
+		init_list(&change->new_data);
+	};
+}
+
+void update_rollback(changesets_t *chgs, zone_contents_t **new_contents)
+{
+	if (chgs != NULL) {
+		changeset_t *change = NULL;
+		WALK_LIST(change, chgs->sets) {
+			// Delete new RR data
+			rrs_list_clear(&change->new_data, NULL);
+			init_list(&change->new_data);
+			// Keep old RR data
+			ptrlist_free(&change->old_data, NULL);
+			init_list(&change->old_data);
+		};
+	}
+	if (new_contents) {
+		update_free_old_zone(new_contents);
+	}
+}
+
+void update_free_old_zone(zone_contents_t **contents)
+{
+	/*
+	 * Free the zone tree, but only the structure
+	 * (nodes are already destroyed) and free additional arrays.
+	 */
+	knot_zone_tree_apply((*contents)->nodes, free_additional, NULL);
+	knot_zone_tree_deep_free(&(*contents)->nodes);
+	knot_zone_tree_deep_free(&(*contents)->nsec3_nodes);
+
+	knot_nsec3param_free(&(*contents)->nsec3_params);
+
+	free(*contents);
+	*contents = NULL;
+}
+
diff --git a/src/knot/updates/apply.h b/src/knot/updates/apply.h
new file mode 100644
index 0000000000000000000000000000000000000000..3e9ec88f8ef88604b342c89604236ed25a5f45da
--- /dev/null
+++ b/src/knot/updates/apply.h
@@ -0,0 +1,98 @@
+/*!
+ * \file apply.h
+ *
+ * \author Lubos Slovak <lubos.slovak@nic.cz>
+ * \author Jan Kadlec <jan.kadlec@nic.cz>
+ *
+ * \brief Changesets application and update helpers.
+ *
+ * \addtogroup xfr
+ * @{
+ */
+/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <stdint.h>
+#include <string.h>
+
+#include "knot/zone/zone.h"
+#include "knot/server/xfr-handler.h"
+#include "knot/updates/changesets.h"
+
+/*!
+ * \brief Checks if a zone transfer is required by comparing the zone's SOA with
+ *        the one received from master server.
+ *
+ * \param zone Zone to check.
+ * \param soa_response Response to SOA query received from master server.
+ *
+ * \retval < 0 if an error occured.
+ * \retval 1 if the transfer is needed.
+ * \retval 0 if the transfer is not needed.
+ */
+int xfrin_transfer_needed(const zone_contents_t *zone,
+                          knot_pkt_t *soa_response);
+
+/*!
+ * \brief Applies changesets *with* zone shallow copy.
+ *
+ * \param zone          Zone to be updated.
+ * \param chsets        Changes to be made.
+ * \param new_contents  New zone will be returned using this arg.
+ *
+ * \return KNOT_E*
+ */
+int apply_changesets(zone_t *zone, changesets_t *chsets,
+                     zone_contents_t **new_contents);
+
+/*!
+ * \brief Applies changesets directly to the zone, without copying it.
+ *
+ * \param contents Zone contents to apply the changesets to. Will be modified.
+ * \param chsets   Changesets to be applied to the zone.
+ *
+ * \retval KNOT_EOK if successful.
+ * \retval KNOT_EINVAL if given one of the arguments is NULL.
+ * \return Other error code if the application went wrong.
+ */
+int apply_changesets_directly(zone_contents_t *contents,
+                              changesets_t *chsets);
+
+/*!
+ * \brief Cleanups successful update. (IXFR, DNSSEC, DDNS).
+ * \param chgs  Changesets used to create the update.
+ */
+void update_cleanup(changesets_t *chgs);
+
+/*!
+ * \brief Rollbacks failed update (IXFR, DNSSEC, DDNS).
+ *
+ * \param chgs          Changesets used to create the update.
+ * \param new_contents  Created zone contents.
+ */
+void update_rollback(changesets_t *chgs, zone_contents_t **new_contents);
+
+/*!
+ * \brief Frees old zone contents - i.e. contents that were used to create the
+ *        shallow copy, but are now obsolete.
+ * \note Exported because of update.c, zone.c.
+ * \param contents  Contents to free.
+ */
+void update_free_old_zone(zone_contents_t **contents);
+
+/*! @} */
diff --git a/src/knot/updates/changesets.c b/src/knot/updates/changesets.c
index b4b6ac01438571e34291036f76805d5a5ddce849..bd6669398a3f437f2d24acb205b813fc4b71532e 100644
--- a/src/knot/updates/changesets.c
+++ b/src/knot/updates/changesets.c
@@ -27,17 +27,17 @@
 #include "libknot/rdata/soa.h"
 #include "common/debug.h"
 
-static int knot_changesets_init(knot_changesets_t *changesets)
+static int knot_changesets_init(changesets_t *changesets)
 {
 	if (changesets == NULL) {
 		return KNOT_EINVAL;
 	}
 
 	// Create new changesets structure
-	memset(changesets, 0, sizeof(knot_changesets_t));
+	memset(changesets, 0, sizeof(changesets_t));
 
 	// Initialize memory context for changesets (xmalloc'd)
-	struct mempool *chs_pool = mp_new(sizeof(knot_changeset_t));
+	struct mempool *chs_pool = mp_new(sizeof(changeset_t));
 	changesets->mmc_chs.ctx = chs_pool;
 	changesets->mmc_chs.alloc = (mm_alloc_t)mp_alloc;
 	changesets->mmc_chs.free = NULL;
@@ -54,18 +54,18 @@ static int knot_changesets_init(knot_changesets_t *changesets)
 	return KNOT_EOK;
 }
 
-knot_changesets_t *knot_changesets_create(unsigned count)
+changesets_t *changesets_create(unsigned count)
 {
-	knot_changesets_t *ch = malloc(sizeof(knot_changesets_t));
+	changesets_t *ch = malloc(sizeof(changesets_t));
 	int ret = knot_changesets_init(ch);
 	if (ret != KNOT_EOK) {
 		return NULL;
 	}
 
 	for (unsigned i = 0; i < count; ++i) {
-		knot_changeset_t *change = knot_changesets_create_changeset(ch);
+		changeset_t *change = changesets_create_changeset(ch);
 		if (change == NULL) {
-			knot_changesets_free(&ch);
+			changesets_free(&ch, NULL);
 			return NULL;
 		}
 	}
@@ -73,19 +73,19 @@ knot_changesets_t *knot_changesets_create(unsigned count)
 	return ch;
 }
 
-knot_changeset_t *knot_changesets_create_changeset(knot_changesets_t *ch)
+changeset_t *changesets_create_changeset(changesets_t *ch)
 {
 	if (ch == NULL) {
 		return NULL;
 	}
 
 	// Create set changesets' memory allocator
-	knot_changeset_t *set = ch->mmc_chs.alloc(ch->mmc_chs.ctx,
-	                                          sizeof(knot_changeset_t));
+	changeset_t *set = ch->mmc_chs.alloc(ch->mmc_chs.ctx,
+	                                          sizeof(changeset_t));
 	if (set == NULL) {
 		return NULL;
 	}
-	memset(set, 0, sizeof(knot_changeset_t));
+	memset(set, 0, sizeof(changeset_t));
 
 	// Init set's memory context (Allocator from changests structure is used)
 	set->mem_ctx = ch->mmc_rr;
@@ -106,27 +106,27 @@ knot_changeset_t *knot_changesets_create_changeset(knot_changesets_t *ch)
 	return set;
 }
 
-knot_changeset_t *knot_changesets_get_last(const knot_changesets_t *chs)
+changeset_t *changesets_get_last(const changesets_t *chs)
 {
 	if (chs == NULL || EMPTY_LIST(chs->sets)) {
 		return NULL;
 	}
 
-	return (knot_changeset_t *)(TAIL(chs->sets));
+	return (changeset_t *)(TAIL(chs->sets));
 }
 
-bool knot_changesets_empty(const knot_changesets_t *chs)
+bool changesets_empty(const changesets_t *chs)
 {
-	knot_changeset_t *last = knot_changesets_get_last(chs);
+	changeset_t *last = changesets_get_last(chs);
 	if (last == NULL) {
 		return true;
 	}
 
-	return knot_changeset_is_empty(last);
+	return changeset_is_empty(last);
 }
 
-int knot_changeset_add_rrset(knot_changeset_t *chgs, knot_rrset_t *rrset,
-                             knot_changeset_part_t part)
+int changeset_add_rrset(changeset_t *chgs, knot_rrset_t *rrset,
+                             changeset_part_t part)
 {
 	// Create wrapper node for list
 	knot_rr_ln_t *rr_node =
@@ -138,7 +138,7 @@ int knot_changeset_add_rrset(knot_changeset_t *chgs, knot_rrset_t *rrset,
 	}
 	rr_node->rr = rrset;
 
-	if (part == KNOT_CHANGESET_ADD) {
+	if (part == CHANGESET_ADD) {
 		add_tail(&chgs->add, (node_t *)rr_node);
 	} else {
 		add_tail(&chgs->remove, (node_t *)rr_node);
@@ -154,15 +154,15 @@ static void knot_changeset_store_soa(knot_rrset_t **chg_soa,
 	*chg_serial = knot_soa_serial(&soa->rrs);
 }
 
-void knot_changeset_add_soa(knot_changeset_t *changeset, knot_rrset_t *soa,
-                            knot_changeset_part_t part)
+void changeset_add_soa(changeset_t *changeset, knot_rrset_t *soa,
+                            changeset_part_t part)
 {
 	switch (part) {
-	case KNOT_CHANGESET_ADD:
+	case CHANGESET_ADD:
 		knot_changeset_store_soa(&changeset->soa_to,
 		                          &changeset->serial_to, soa);
 		break;
-	case KNOT_CHANGESET_REMOVE:
+	case CHANGESET_REMOVE:
 		knot_changeset_store_soa(&changeset->soa_from,
 		                          &changeset->serial_from, soa);
 		break;
@@ -171,7 +171,7 @@ void knot_changeset_add_soa(knot_changeset_t *changeset, knot_rrset_t *soa,
 	}
 }
 
-bool knot_changeset_is_empty(const knot_changeset_t *changeset)
+bool changeset_is_empty(const changeset_t *changeset)
 {
 	if (changeset == NULL) {
 		return true;
@@ -181,17 +181,17 @@ bool knot_changeset_is_empty(const knot_changeset_t *changeset)
 	        EMPTY_LIST(changeset->add) && EMPTY_LIST(changeset->remove));
 }
 
-size_t knot_changeset_size(const knot_changeset_t *changeset)
+size_t changeset_size(const changeset_t *changeset)
 {
-	if (!changeset || knot_changeset_is_empty(changeset)) {
+	if (!changeset || changeset_is_empty(changeset)) {
 		return 0;
 	}
 
 	return list_size(&changeset->add) + list_size(&changeset->remove);
 }
 
-int knot_changeset_apply(knot_changeset_t *changeset,
-                         knot_changeset_part_t part,
+int changeset_apply(changeset_t *changeset,
+                         changeset_part_t part,
                          int (*func)(knot_rrset_t *, void *), void *data)
 {
 	if (changeset == NULL || func == NULL) {
@@ -199,14 +199,14 @@ int knot_changeset_apply(knot_changeset_t *changeset,
 	}
 
 	knot_rr_ln_t *rr_node = NULL;
-	if (part == KNOT_CHANGESET_ADD) {
+	if (part == CHANGESET_ADD) {
 		WALK_LIST(rr_node, changeset->add) {
 			int res = func(rr_node->rr, data);
 			if (res != KNOT_EOK) {
 				return res;
 			}
 		}
-	} else if (part == KNOT_CHANGESET_REMOVE) {
+	} else if (part == CHANGESET_REMOVE) {
 		WALK_LIST(rr_node, changeset->remove) {
 			int res = func(rr_node->rr, data);
 			if (res != KNOT_EOK) {
@@ -218,7 +218,7 @@ int knot_changeset_apply(knot_changeset_t *changeset,
 	return KNOT_EOK;
 }
 
-int knot_changeset_merge(knot_changeset_t *ch1, knot_changeset_t *ch2)
+int changeset_merge(changeset_t *ch1, changeset_t *ch2)
 {
 	if (ch1 == NULL || ch2 == NULL || ch1->data != NULL || ch2->data != NULL) {
 		return KNOT_EINVAL;
@@ -238,7 +238,7 @@ int knot_changeset_merge(knot_changeset_t *ch1, knot_changeset_t *ch2)
 	return KNOT_EOK;
 }
 
-static void knot_free_changeset(knot_changeset_t *changeset)
+static void knot_free_changeset(changeset_t *changeset)
 {
 	if (changeset == NULL) {
 		return;
@@ -260,10 +260,10 @@ static void knot_free_changeset(knot_changeset_t *changeset)
 	free(changeset->data);
 }
 
-static void knot_changesets_deinit(knot_changesets_t *changesets)
+static void knot_changesets_deinit(changesets_t *changesets, mm_ctx_t *rr_mm)
 {
 	if (!EMPTY_LIST(changesets->sets)) {
-		knot_changeset_t *chg = NULL;
+		changeset_t *chg = NULL;
 		WALK_LIST(chg, changesets->sets) {
 			knot_free_changeset(chg);
 		}
@@ -274,28 +274,28 @@ static void knot_changesets_deinit(knot_changesets_t *changesets)
 	// Free pool with RRs in sets / changes
 	mp_delete(changesets->mmc_rr.ctx);
 
-	knot_rrset_free(&changesets->first_soa, NULL);
+	knot_rrset_free(&changesets->first_soa, rr_mm);
 }
 
-void knot_changesets_free(knot_changesets_t **changesets)
+void changesets_free(changesets_t **changesets, mm_ctx_t *rr_mm)
 {
 	if (changesets == NULL || *changesets == NULL) {
 		return;
 	}
 
-	knot_changesets_deinit(*changesets);
+	knot_changesets_deinit(*changesets, rr_mm);
 
 	free(*changesets);
 	*changesets = NULL;
 }
 
-int knot_changesets_clear(knot_changesets_t *changesets)
+int changesets_clear(changesets_t *changesets, mm_ctx_t *rr_mm)
 {
 	if (changesets == NULL) {
 		return KNOT_EINVAL;
 	}
 
-	knot_changesets_deinit(changesets);
+	knot_changesets_deinit(changesets, rr_mm);
 	return knot_changesets_init(changesets);
 }
 
diff --git a/src/knot/updates/changesets.h b/src/knot/updates/changesets.h
index 5d3f9bab4f8d4000e23d5e9a5c001e266124181b..0cc872db0f410beeb103a2bffd6766902154dbab 100644
--- a/src/knot/updates/changesets.h
+++ b/src/knot/updates/changesets.h
@@ -34,15 +34,8 @@
 
 /*----------------------------------------------------------------------------*/
 
-/*! \brief Changeset flags, stored as first 4 bytes in serialized changeset. */
-typedef enum {
-	KNOT_CHANGESET_TYPE_IXFR   = 1 << 0,
-	KNOT_CHANGESET_TYPE_DDNS   = 1 << 1,
-	KNOT_CHANGESET_TYPE_DNSSEC = 1 << 2
-} knot_changeset_flag_t;
-
 /*! \brief One changeset received from wire, with parsed RRs. */
-typedef struct knot_changeset {
+typedef struct changeset {
 	node_t n; /*!< List node. */
 	mm_ctx_t mem_ctx; /*!< Memory context */
 	knot_rrset_t *soa_from; /*!< Start SOA. */
@@ -53,10 +46,9 @@ typedef struct knot_changeset {
 	size_t size; /*!< Size of serialized changeset. */
 	uint32_t serial_from; /*!< SOA start serial. */
 	uint32_t serial_to; /*!< SOA destination serial. */
-	uint32_t flags; /*!< DDNS / IXFR flags. */
 	list_t old_data; /*!< Old data, to be freed after succesfull update. */
 	list_t new_data; /*!< New data, to be freed after failed update. */
-} knot_changeset_t;
+} changeset_t;
 
 /*----------------------------------------------------------------------------*/
 
@@ -79,14 +71,14 @@ typedef struct {
 	size_t count; /*!< Changeset count. */
 	knot_rrset_t *first_soa; /*!< First received SOA. */
 	uint32_t flags; /*!< DDNS / IXFR flags. */
-} knot_changesets_t;
+} changesets_t;
 
 /*----------------------------------------------------------------------------*/
 
 typedef enum {
-	KNOT_CHANGESET_ADD,
-	KNOT_CHANGESET_REMOVE
-} knot_changeset_part_t;
+	CHANGESET_ADD,
+	CHANGESET_REMOVE
+} changeset_part_t;
 
 /*----------------------------------------------------------------------------*/
 
@@ -99,17 +91,10 @@ typedef enum {
  * \retval Created structure on success.
  * \retval NULL on failure.
  */
-knot_changesets_t *knot_changesets_create(unsigned count);
-
-/*!
- * \brief Frees the 'changesets' structure, including all its internal data.
- *
- * \param changesets Double pointer to changesets structure to be freed.
- */
-void knot_changesets_free(knot_changesets_t **changesets);
+changesets_t *changesets_create(unsigned count);
 
 /*! \brief Reinitialize changesets structure. */
-int knot_changesets_clear(knot_changesets_t *changesets);
+int changesets_clear(changesets_t *changesets, mm_ctx_t *rr_mm);
 
 /*!
  * \brief Creates new changeset structure and returns it to caller.
@@ -120,7 +105,7 @@ int knot_changesets_clear(knot_changesets_t *changesets);
  * \retval Created structure on success.
  * \retval NULL on failure.
  */
-knot_changeset_t *knot_changesets_create_changeset(knot_changesets_t *ch);
+changeset_t *changesets_create_changeset(changesets_t *ch);
 
 /*!
  * \brief Gets last changesets from from structure's list.
@@ -130,10 +115,10 @@ knot_changeset_t *knot_changesets_create_changeset(knot_changesets_t *ch);
  * \retval Last changeset on success.
  * \retval NULL on failure.
  */
-knot_changeset_t *knot_changesets_get_last(const knot_changesets_t *ch);
+changeset_t *changesets_get_last(const changesets_t *ch);
 
 /*! \brief Return true if changesets are empty. */
-bool knot_changesets_empty(const knot_changesets_t *chs);
+bool changesets_empty(const changesets_t *chs);
 
 /*!
  * \brief Add RRSet to changeset. RRSet is either inserted to 'add' or to
@@ -146,8 +131,8 @@ bool knot_changesets_empty(const knot_changesets_t *chs);
  * \retval KNOT_EOK on success.
  * \retval Error code on failure.
  */
-int knot_changeset_add_rrset(knot_changeset_t *chgs,
-                             knot_rrset_t *rrset, knot_changeset_part_t part);
+int changeset_add_rrset(changeset_t *chgs,
+                        knot_rrset_t *rrset, changeset_part_t part);
 
 /*!
  * \brief Adds a source/destination SOA RRSet to changeset.
@@ -156,8 +141,8 @@ int knot_changeset_add_rrset(knot_changeset_t *chgs,
  * \param soa SOA RRSet to be stored to changeset.
  * \param part To which part we store SOA (from = REMOVE, add = TO)
  */
-void knot_changeset_add_soa(knot_changeset_t *changeset, knot_rrset_t *soa,
-                            knot_changeset_part_t part);
+void changeset_add_soa(changeset_t *changeset, knot_rrset_t *soa,
+                       changeset_part_t part);
 
 /*!
  * \brief Checks whether changeset is empty.
@@ -170,7 +155,7 @@ void knot_changeset_add_soa(knot_changeset_t *changeset, knot_rrset_t *soa,
  * \retval true if changeset is empty.
  * \retval false if changeset is not empty.
  */
-bool knot_changeset_is_empty(const knot_changeset_t *changeset);
+bool changeset_is_empty(const changeset_t *changeset);
 
 /*!
  * \brief Get number of changes (additions and removals) in the changeset.
@@ -179,7 +164,7 @@ bool knot_changeset_is_empty(const knot_changeset_t *changeset);
  *
  * \return Number of changes in the changeset.
  */
-size_t knot_changeset_size(const knot_changeset_t *changeset);
+size_t changeset_size(const changeset_t *changeset);
 
 /*!
  * \brief Apply given function to all RRSets in one part of the changeset.
@@ -197,16 +182,17 @@ size_t knot_changeset_size(const knot_changeset_t *changeset);
  * \retval KNOT_EINVAL if \a changeset or \a func is NULL.
  * \retval Other error code if the applied function failed.
  */
-int knot_changeset_apply(knot_changeset_t *changeset,
-                         knot_changeset_part_t part,
-                         int (*func)(knot_rrset_t *, void *), void *data);
+int changeset_apply(changeset_t *changeset,
+                    changeset_part_t part,
+                    int (*func)(knot_rrset_t *, void *), void *data);
 
 /*!
  * \brief Frees the 'changesets' structure, including all its internal data.
  *
- * \param changesets Double pointer to changesets structure to be freed.
+ * \param changesets  Double pointer to changesets structure to be freed.
+ * \param mm          Memory context used to allocate RRSets.
  */
-void knot_changesets_free(knot_changesets_t **changesets);
+void changesets_free(changesets_t **changesets, mm_ctx_t *rr_mm);
 
 /*!
  * \brief Merges two changesets together, second changeset's lists are kept.
@@ -221,7 +207,7 @@ void knot_changesets_free(knot_changesets_t **changesets);
  * \retval KNOT_EOK on success.
  * \retval Error code on failure.
  */
-int knot_changeset_merge(knot_changeset_t *ch1, knot_changeset_t *ch2);
+int changeset_merge(changeset_t *ch1, changeset_t *ch2);
 
 #endif /* _KNOT_CHANGESETS_H_ */
 
diff --git a/src/knot/updates/ddns.c b/src/knot/updates/ddns.c
index 066a6d70e8ea534cfd6e78c9f12f8794db1fba47..c2a8843732895ad453b0bbb8431c4e7cc56f8d2e 100644
--- a/src/knot/updates/ddns.c
+++ b/src/knot/updates/ddns.c
@@ -20,7 +20,7 @@
 
 #include "knot/updates/ddns.h"
 #include "knot/updates/changesets.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "knot/zone/semantic-check.h"
 #include "common/debug.h"
 #include "libknot/packet/pkt.h"
@@ -31,6 +31,8 @@
 #include "common/descriptor.h"
 #include "common/lists.h"
 
+#warning merge with update.c
+
 /* ----------------------------- prereq check ------------------------------- */
 
 /*!< \brief Clears prereq RRSet list. */
@@ -279,14 +281,14 @@ static int process_prereq(const knot_rrset_t *rrset, uint16_t qclass,
 	};
 
 /*!< \brief Checks whether RR was already removed. */
-static bool removed_rr(const knot_changeset_t *changeset, const knot_rrset_t *rr)
+static bool removed_rr(const changeset_t *changeset, const knot_rrset_t *rr)
 {
 	LIST_MATCH(changeset->remove,
 	           knot_rrset_equal(rr, list_rr, KNOT_RRSET_COMPARE_WHOLE));
 }
 
 /*!< \brief Checks whether any CNAME RR under dname was added. */
-static bool cname_added(const knot_changeset_t *changeset, const knot_dname_t *d)
+static bool cname_added(const changeset_t *changeset, const knot_dname_t *d)
 {
 	knot_rrset_t mock_cname;
 	knot_rrset_init(&mock_cname, (knot_dname_t *)d,
@@ -296,7 +298,7 @@ static bool cname_added(const knot_changeset_t *changeset, const knot_dname_t *d
 }
 
 /*!< \brief Checks whether any RR under given name was added. */
-static bool name_added(const knot_changeset_t *changeset, const knot_dname_t *d)
+static bool name_added(const changeset_t *changeset, const knot_dname_t *d)
 {
 	LIST_MATCH(changeset->add, knot_dname_is_equal(d, list_rr->owner));
 }
@@ -360,7 +362,7 @@ static bool should_replace(const knot_rrset_t *chg_rrset,
 
 /*!< \brief Returns true if node will be empty after update application. */
 static bool node_empty(const zone_node_t *node, knot_dname_t *owner,
-                       const knot_changeset_t *changeset)
+                       const changeset_t *changeset)
 {
 	if (node == NULL && name_added(changeset, owner)) {
 		// Node created in update.
@@ -413,7 +415,7 @@ static bool node_contains_rr(const zone_node_t *node,
 /*!< \brief Returns true if CNAME is in this node. */
 static bool adding_to_cname(const knot_dname_t *owner,
                             const zone_node_t *node,
-                            knot_changeset_t *changeset)
+                            changeset_t *changeset)
 {
 	if (cname_added(changeset, owner)) {
 		// Added a CNAME in this update.
@@ -455,7 +457,7 @@ static bool skip_soa(const knot_rrset_t *rr, int64_t sn)
 /* ---------------------- changeset manipulation ---------------------------- */
 
 /*!< \brief Checks whether record should be added or replaced. */
-static bool skip_record_addition(knot_changeset_t *changeset,
+static bool skip_record_addition(changeset_t *changeset,
                                  knot_rrset_t *rr)
 {
 	knot_rr_ln_t *rr_node = NULL;
@@ -477,7 +479,7 @@ static bool skip_record_addition(knot_changeset_t *changeset,
 }
 
 /*!< \brief Adds RR into add section of changeset if it is deemed worthy. */
-static int add_rr_to_chgset(const knot_rrset_t *rr, knot_changeset_t *changeset,
+static int add_rr_to_chgset(const knot_rrset_t *rr, changeset_t *changeset,
                             int *apex_ns_rem)
 {
 	knot_rrset_t *rr_copy = knot_rrset_copy(rr, NULL);
@@ -496,11 +498,11 @@ static int add_rr_to_chgset(const knot_rrset_t *rr, knot_changeset_t *changeset,
 		(*apex_ns_rem)--;
 	}
 
-	return knot_changeset_add_rrset(changeset, rr_copy, KNOT_CHANGESET_ADD);
+	return changeset_add_rrset(changeset, rr_copy, CHANGESET_ADD);
 }
 
 /*!< \brief Checks whether record should be removed (duplicate check). */
-static bool skip_record_removal(knot_changeset_t *changeset, knot_rrset_t *rr)
+static bool skip_record_removal(changeset_t *changeset, knot_rrset_t *rr)
 {
 	knot_rr_ln_t *rr_node = NULL;
 	WALK_LIST(rr_node, changeset->remove) {
@@ -516,7 +518,7 @@ static bool skip_record_removal(knot_changeset_t *changeset, knot_rrset_t *rr)
 }
 
 /*!< \brief Adds RR into remove section of changeset if it is deemed worthy. */
-static int rem_rr_to_chgset(const knot_rrset_t *rr, knot_changeset_t *changeset,
+static int rem_rr_to_chgset(const knot_rrset_t *rr, changeset_t *changeset,
                             int *apex_ns_rem)
 {
 	knot_rrset_t *rr_copy = knot_rrset_copy(rr, NULL);
@@ -535,12 +537,12 @@ static int rem_rr_to_chgset(const knot_rrset_t *rr, knot_changeset_t *changeset,
 		(*apex_ns_rem)++;
 	}
 
-	return knot_changeset_add_rrset(changeset, rr_copy, KNOT_CHANGESET_REMOVE);
+	return changeset_add_rrset(changeset, rr_copy, CHANGESET_REMOVE);
 }
 
 /*!< \brief Adds all RRs from RRSet into remove section of changeset. */
 static int rem_rrset_to_chgset(const knot_rrset_t *rrset,
-                               knot_changeset_t *changeset,
+                               changeset_t *changeset,
                                int *apex_ns_rem)
 {
 	knot_rrset_t rr;
@@ -568,7 +570,7 @@ static int rem_rrset_to_chgset(const knot_rrset_t *rrset,
 /*!< \brief Processes CNAME addition (replace or ignore) */
 static int process_add_cname(const zone_node_t *node,
                              const knot_rrset_t *rr,
-                             knot_changeset_t *changeset)
+                             changeset_t *changeset)
 {
 	knot_rrset_t cname = node_rrset(node, KNOT_RRTYPE_CNAME);
 	if (!knot_rrset_empty(&cname)) {
@@ -595,7 +597,7 @@ static int process_add_cname(const zone_node_t *node,
 /*!< \brief Processes CNAME addition (ignore when not removed, or non-apex) */
 static int process_add_nsec3param(const zone_node_t *node,
                                   const knot_rrset_t *rr,
-                                  knot_changeset_t *changeset)
+                                  changeset_t *changeset)
 {
 	if (node == NULL || !node_rrtype_exists(node, KNOT_RRTYPE_SOA)) {
 		// Ignore non-apex additions
@@ -625,7 +627,7 @@ static int process_add_nsec3param(const zone_node_t *node,
  */
 static int process_add_soa(const zone_node_t *node,
                            const knot_rrset_t *rr,
-                           knot_changeset_t *changeset)
+                           changeset_t *changeset)
 {
 	if (node == NULL || !node_rrtype_exists(node, KNOT_RRTYPE_SOA)) {
 		// Adding SOA to non-apex node, ignore
@@ -649,14 +651,14 @@ static int process_add_soa(const zone_node_t *node,
 		return KNOT_ENOMEM;
 	}
 
-	knot_changeset_add_soa(changeset, soa_cpy, KNOT_CHANGESET_ADD);
+	changeset_add_soa(changeset, soa_cpy, CHANGESET_ADD);
 	return KNOT_EOK;
 }
 
 /*!< \brief Adds normal RR, ignores when CNAME exists in node. */
 static int process_add_normal(const zone_node_t *node,
                               const knot_rrset_t *rr,
-                              knot_changeset_t *changeset,
+                              changeset_t *changeset,
                               int *apex_ns_rem)
 {
 	if (adding_to_cname(rr->owner, node, changeset)) {
@@ -679,7 +681,7 @@ static int process_add_normal(const zone_node_t *node,
 /*!< \brief Decides what to do with RR addition. */
 static int process_add(const knot_rrset_t *rr,
                        const zone_node_t *node,
-                       knot_changeset_t *changeset,
+                       changeset_t *changeset,
                        int *apex_ns_rem)
 {
 	switch(rr->type) {
@@ -699,7 +701,7 @@ static int process_add(const knot_rrset_t *rr,
 /*!< \brief Removes single RR from zone. */
 static int process_rem_rr(const knot_rrset_t *rr,
                           const zone_node_t *node,
-                          knot_changeset_t *changeset,
+                          changeset_t *changeset,
                           int *apex_ns_rem)
 {
 	const bool apex_ns = node_rrtype_exists(node, KNOT_RRTYPE_SOA) &&
@@ -751,7 +753,7 @@ static int process_rem_rr(const knot_rrset_t *rr,
 /*!< \brief Removes RRSet from zone. */
 static int process_rem_rrset(const knot_rrset_t *rrset,
                              const zone_node_t *node,
-                             knot_changeset_t *changeset)
+                             changeset_t *changeset)
 {
 	if (rrset->type == KNOT_RRTYPE_SOA ||
 	    knot_rrtype_is_ddns_forbidden(rrset->type)) {
@@ -782,7 +784,7 @@ static int process_rem_rrset(const knot_rrset_t *rrset,
 
 /*!< \brief Removes node from zone. */
 static int process_rem_node(const knot_rrset_t *rr,
-                            const zone_node_t *node, knot_changeset_t *changeset)
+                            const zone_node_t *node, changeset_t *changeset)
 {
 	// Remove all previously added records with given owner from changeset
 	remove_owner_from_list(&changeset->add, rr->owner);
@@ -806,7 +808,7 @@ static int process_rem_node(const knot_rrset_t *rr,
 /*!< \brief Decides what to with removal. */
 static int process_remove(const knot_rrset_t *rr,
                           const zone_node_t *node,
-                          knot_changeset_t *changeset,
+                          changeset_t *changeset,
                           int *apex_ns_rem)
 {
 	if (is_rr_removal(rr)) {
@@ -901,7 +903,7 @@ static int check_update(const knot_rrset_t *rrset, const knot_pkt_t *query,
 
 /*!< \brief Checks RR and decides what to do with it. */
 static int process_rr(const knot_rrset_t *rr, const zone_contents_t *zone,
-                      knot_changeset_t *changeset, int *apex_ns_rem)
+                      changeset_t *changeset, int *apex_ns_rem)
 {
 	const zone_node_t *node = zone_contents_find_node(zone, rr->owner);
 
@@ -963,7 +965,7 @@ int ddns_process_prereqs(const knot_pkt_t *query, const zone_contents_t *zone,
 }
 
 int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
-                        knot_changeset_t *changeset, uint16_t *rcode)
+                        changeset_t *changeset, uint16_t *rcode)
 {
 	if (zone == NULL || query == NULL || changeset == NULL || rcode == NULL) {
 		return KNOT_EINVAL;
@@ -975,7 +977,7 @@ int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
 		return KNOT_ENOMEM;
 	}
 
-	knot_changeset_add_soa(changeset, soa_begin, KNOT_CHANGESET_REMOVE);
+	changeset_add_soa(changeset, soa_begin, CHANGESET_REMOVE);
 
 	int64_t sn_old = zone_contents_serial(zone->contents);
 
@@ -1005,7 +1007,7 @@ int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
 
 	if (changeset->soa_to == NULL) {
 		// No SOA in the update, create according to current policy
-		if (knot_changeset_is_empty(changeset)) {
+		if (changeset_is_empty(changeset)) {
 			// No change, no new SOA
 			return KNOT_EOK;
 		}
@@ -1019,7 +1021,7 @@ int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
 		int new_serial = zone_contents_next_serial(zone->contents,
 		                                           zone->conf->serial_policy);
 		knot_soa_serial_set(&soa_cpy->rrs, new_serial);
-		knot_changeset_add_soa(changeset, soa_cpy, KNOT_CHANGESET_ADD);
+		changeset_add_soa(changeset, soa_cpy, CHANGESET_ADD);
 	}
 
 	return KNOT_EOK;
diff --git a/src/knot/updates/ddns.h b/src/knot/updates/ddns.h
index 9b89de05b2141ce5773949f5fe94b5c6cb358688..f7b1a3c13b702cdec824f0ff290bdf85edbb2732 100644
--- a/src/knot/updates/ddns.h
+++ b/src/knot/updates/ddns.h
@@ -57,7 +57,7 @@ int ddns_process_prereqs(const knot_pkt_t *query, const zone_contents_t *zone,
  * \return KNOT_E*
  */
 int ddns_process_update(const zone_t *zone, const knot_pkt_t *query,
-                        knot_changeset_t *changeset, uint16_t *rcode);
+                        changeset_t *changeset, uint16_t *rcode);
 
 #endif /* _KNOT_DDNS_H_ */
 
diff --git a/src/knot/updates/xfr-in.c b/src/knot/updates/xfr-in.c
deleted file mode 100644
index 511509c1ccb9392a9d39e38148866db00c46054f..0000000000000000000000000000000000000000
--- a/src/knot/updates/xfr-in.c
+++ /dev/null
@@ -1,1211 +0,0 @@
-/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#include <assert.h>
-#include <urcu.h>
-
-#include "knot/server/journal.h"
-
-#include "knot/updates/xfr-in.h"
-
-#include "libknot/packet/wire.h"
-#include "common/debug.h"
-#include "libknot/packet/pkt.h"
-#include "libknot/dname.h"
-#include "knot/zone/zone.h"
-#include "knot/zone/zonefile.h"
-#include "knot/dnssec/zone-nsec.h"
-#include "knot/dnssec/zone-sign.h"
-#include "libknot/dnssec/random.h"
-#include "libknot/common.h"
-#include "knot/updates/changesets.h"
-#include "libknot/rdata/tsig.h"
-#include "libknot/tsig-op.h"
-#include "knot/zone/semantic-check.h"
-#include "common/lists.h"
-#include "common/descriptor.h"
-#include "libknot/util/utils.h"
-#include "libknot/rdata/soa.h"
-#include "knot/nameserver/axfr.h"
-
-#define KNOT_NS_TSIG_FREQ 100
-
-/*!
- * \brief Post update cleanup: frees data that are in the tree that will not
- *        be used (old tree if success, new tree if failure).
- *          Freed data:
- *           - actual data inside knot_rrs_t. (the rest is part of the node)
- */
-static void rrs_list_clear(list_t *l, mm_ctx_t *mm)
-{
-	ptrnode_t *n;
-	node_t *nxt;
-	WALK_LIST_DELSAFE(n, nxt, *l) {
-		mm_free(mm, (void *)n->d);
-		mm_free(mm, n);
-	};
-}
-
-static int knot_ns_tsig_required(int packet_nr)
-{
-	/*! \bug This can overflow to negative numbers. Proper solution is to
-	 *       count exactly at one place for each incoming/outgoing packet
-	 *       with packet_nr = (packet_nr + 1) % FREQ and require TSIG on 0.
-	 */
-	dbg_ns_verb("ns_tsig_required(%d): %d\n", packet_nr,
-	            (packet_nr % KNOT_NS_TSIG_FREQ == 0));
-	return (packet_nr % KNOT_NS_TSIG_FREQ == 0);
-}
-
-/*----------------------------------------------------------------------------*/
-/* API functions                                                              */
-/*----------------------------------------------------------------------------*/
-
-int xfrin_transfer_needed(const zone_contents_t *zone,
-                          knot_pkt_t *soa_response)
-{
-	/*
-	 * Retrieve the local Serial
-	 */
-	const knot_rdataset_t *soa_rrs =
-		node_rdataset(zone->apex, KNOT_RRTYPE_SOA);
-	if (soa_rrs == NULL) {
-		char *name = knot_dname_to_str(zone->apex->owner);
-		dbg_xfrin("SOA RRSet missing in the zone %s!\n", name);
-		free(name);
-		return KNOT_ERROR;
-	}
-
-	uint32_t local_serial = knot_soa_serial(soa_rrs);
-	/*
-	 * Retrieve the remote Serial
-	 */
-	// the SOA should be the first (and only) RRSet in the response
-	const knot_pktsection_t *answer = knot_pkt_section(soa_response, KNOT_ANSWER);
-	if (answer->count < 1) {
-		return KNOT_EMALF;
-	}
-	knot_rrset_t soa_rr = answer->rr[0];
-	if (soa_rr.type != KNOT_RRTYPE_SOA) {
-		return KNOT_EMALF;
-	}
-
-	uint32_t remote_serial = knot_soa_serial(&soa_rr.rrs);
-	return (knot_serial_compare(local_serial, remote_serial) < 0);
-}
-
-/*----------------------------------------------------------------------------*/
-
-static int xfrin_check_tsig(knot_pkt_t *packet, knot_ns_xfr_t *xfr,
-                            int tsig_req)
-{
-	assert(packet != NULL);
-	assert(xfr != NULL);
-
-	dbg_xfrin_verb("xfrin_check_tsig(): packet nr: %d, required: %d\n",
-		       xfr->packet_nr, tsig_req);
-
-	/*
-	 * If we are expecting it (i.e. xfr->prev_digest_size > 0)
-	 *   a) it should be there (first, last or each 100th packet) and it
-	 *      is not
-	 *        Then we should discard the changes and close the connection.
-	 *   b) it should be there and it is or it may not be there (other
-	 *      packets) and it is
-	 *        We validate the TSIG and reset packet number counting and
-	 *        data aggregation.
-	 *
-	 * If we are not expecting it (i.e. xfr->prev_digest_size <= 0) and
-	 * it is there => it should probably be considered an error
-	 */
-
-	int ret = KNOT_EOK;
-	if (xfr->tsig_key) {
-		// just append the wireformat to the TSIG data
-		uint8_t *wire_buf = xfr->tsig_data + xfr->tsig_data_size;
-		memcpy(wire_buf, packet->wire, packet->size);
-		xfr->tsig_data_size += packet->size;
-	}
-
-	if (xfr->tsig_key) {
-		if (tsig_req && packet->tsig_rr == NULL) {
-			// TSIG missing!!
-			return KNOT_ENOTSIG;
-		} else if (packet->tsig_rr != NULL) {
-			// TSIG there, either required or not, process
-			if (xfr->packet_nr == 0) {
-				ret = knot_tsig_client_check(packet->tsig_rr,
-					xfr->tsig_data, xfr->tsig_data_size,
-					xfr->digest, xfr->digest_size,
-					xfr->tsig_key,
-					xfr->tsig_prev_time_signed);
-			} else {
-				ret = knot_tsig_client_check_next(packet->tsig_rr,
-					xfr->tsig_data, xfr->tsig_data_size,
-					xfr->digest, xfr->digest_size,
-					xfr->tsig_key,
-					xfr->tsig_prev_time_signed);
-			}
-
-			if (ret != KNOT_EOK) {
-				/* No need to check TSIG error
-				 * here, propagate and check elsewhere.*/
-				return ret;
-			}
-
-			// and reset the data storage
-			//xfr->packet_nr = 1;
-			xfr->tsig_data_size = 0;
-
-			// Extract the digest from the TSIG RDATA and store it.
-			if (xfr->digest_max_size < tsig_rdata_mac_length(packet->tsig_rr)) {
-				return KNOT_ESPACE;
-			}
-			memcpy(xfr->digest, tsig_rdata_mac(packet->tsig_rr),
-			       tsig_rdata_mac_length(packet->tsig_rr));
-			xfr->digest_size = tsig_rdata_mac_length(packet->tsig_rr);
-
-			// Extract the time signed from the TSIG and store it
-			// We may rewrite the tsig_req_time_signed field
-			xfr->tsig_prev_time_signed =
-					tsig_rdata_time_signed(packet->tsig_rr);
-
-		}
-	} else if (packet->tsig_rr != NULL) {
-		// TSIG where it should not be
-		return KNOT_EMALF;
-	}
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-static void xfrin_take_rr(const knot_pktsection_t *answer, const knot_rrset_t **rr, uint16_t *cur)
-{
-	if (*cur < answer->count) {
-		*rr = &answer->rr[*cur];
-		*cur += 1;
-	} else {
-		*rr = NULL;
-	}
-}
-
-/*----------------------------------------------------------------------------*/
-
-int xfrin_process_axfr_packet(knot_pkt_t *pkt, struct xfr_proc *proc)
-{
-	if (pkt == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	int ret = KNOT_EOK;
-	uint16_t rr_id = 0;
-	const knot_rrset_t *rr = NULL;
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-
-	xfrin_take_rr(answer, &rr, &rr_id);
-	++proc->npkts;
-
-	// Init zone creator
-	zcreator_t zc = {.z = proc->zone,
-	                 .master = false, .ret = KNOT_EOK };
-
-	while (rr) {
-		if (rr->type == KNOT_RRTYPE_SOA &&
-		    node_rrtype_exists(zc.z->apex, KNOT_RRTYPE_SOA)) {
-			// Last SOA, last message, check TSIG.
-//			int ret = xfrin_check_tsig(pkt, xfr, 1);
-#warning TODO: TSIG API
-			if (ret != KNOT_EOK) {
-				return ret;
-			}
-			return 1; // Signal that transfer finished.
-		} else {
-			int ret = zcreator_step(&zc, rr);
-			if (ret != KNOT_EOK) {
-				// 'rr' is either inserted, or free'd
-				return ret;
-			}
-			xfrin_take_rr(answer, &rr, &rr_id);
-		}
-	}
-
-	// Check possible TSIG at the end of DNS message.
-//	return xfrin_check_tsig(pkt, xfr, knot_ns_tsig_required(xfr->packet_nr));
-#warning TODO: TSIG API
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-int xfrin_process_ixfr_packet(knot_pkt_t *pkt, knot_ns_xfr_t *xfr)
-{
-	knot_changesets_t **chs = (knot_changesets_t **)(&xfr->data);
-	if (pkt == NULL || chs == NULL) {
-		dbg_xfrin("Wrong parameters supported.\n");
-		return KNOT_EINVAL;
-	}
-
-	uint16_t rr_id = 0;
-	const knot_rrset_t *rr = NULL;
-	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
-	xfrin_take_rr(answer, &rr, &rr_id);
-	if (rr == NULL) {
-		return KNOT_EXFRREFUSED; /* Empty, try again with AXFR */
-	}
-
-	// state of the transfer
-	// -1 .. a SOA is expected to create a new changeset
-	int state = 0;
-	int ret = KNOT_EOK;
-	if (*chs == NULL) {
-		dbg_xfrin_verb("Changesets empty, creating new.\n");
-		*chs = knot_changesets_create(0);
-		if (*chs == NULL) {
-			return KNOT_ENOMEM;
-		}
-
-		// the first RR must be a SOA
-		if (rr->type != KNOT_RRTYPE_SOA) {
-			dbg_xfrin("First RR is not a SOA RR!\n");
-			ret = KNOT_EMALF;
-			goto cleanup;
-		}
-
-		// just store the first SOA for later use
-		(*chs)->first_soa = knot_rrset_copy(rr, NULL);
-		if ((*chs)->first_soa == NULL) {
-			ret = KNOT_ENOMEM;
-			goto cleanup;
-		}
-		state = -1;
-
-		dbg_xfrin_verb("First SOA of IXFR saved, state set to -1.\n");
-
-		// take next RR
-		xfrin_take_rr(answer, &rr, &rr_id);
-
-		/*
-		 * If there is no other records in the response than the SOA, it
-		 * means one of these three cases:
-		 *
-		 * 1) The server does not have newer zone than ours.
-		 *    This is indicated by serial equal to the one of our zone.
-		 * 2) The server wants to send the transfer but is unable to fit
-		 *    it in the packet. This is indicated by serial different
-		 *    (newer) from the one of our zone, but applies only for
-		 *    IXFR/UDP.
-		 * 3) The master is weird and sends only SOA in the first packet
-		 *    of a fallback to AXFR answer (PowerDNS does this).
-		 *
-		 * The serials must be compared in other parts of the server, so
-		 * just indicate that the answer contains only one SOA.
-		 */
-		if (rr == NULL) {
-			dbg_xfrin("Response containing only SOA,\n");
-			return XFRIN_RES_SOA_ONLY;
-		} else if (rr->type != KNOT_RRTYPE_SOA) {
-			dbg_xfrin("Fallback to AXFR.\n");
-			return XFRIN_RES_FALLBACK;
-		}
-	} else {
-		if ((*chs)->first_soa == NULL) {
-			dbg_xfrin("Changesets don't contain SOA first!\n");
-			ret = KNOT_EINVAL;
-			goto cleanup;
-		}
-		dbg_xfrin_detail("Changesets present.\n");
-	}
-
-	/*
-	 * Process the next RR. Different requirements are in place in
-	 * different cases:
-	 *
-	 * 1) Last changeset has both soa_from and soa_to.
-	 *    a) The next RR is a SOA.
-	 *      i) The next RR is equal to the first_soa saved in changesets.
-	 *         This denotes the end of the transfer. It may be dropped and
-	 *         the end should be signalised by returning positive value.
-	 *
-	 *      ii) The next RR is some other SOA.
-	 *          This means a start of new changeset - create it and add it
-	 *          to the list.
-	 *
-	 *    b) The next RR is not a SOA.
-	 *       Put the RR into the ADD part of the last changeset as this is
-	 *       not finished yet. Continue while SOA is not encountered. Then
-	 *       jump to 1-a.
-	 *
-	 * 2) Last changeset has only the soa_from and does not have soa_to.
-	 *    a) The next RR is a SOA.
-	 *       This means start of the ADD section. Put the SOA to the
-	 *       changeset. Continue adding RRs to the ADD section while SOA
-	 *       is not encountered. This is identical to 1-b.
-	 *
-	 *    b) The next RR is not a SOA.
-	 *       This means the REMOVE part is not finished yet. Add the RR to
-	 *       the REMOVE part. Continue adding next RRs until a SOA is
-	 *       encountered. Then jump to 2-a.
-	 */
-
-	// first, find out in what state we are
-	/*! \todo It would be more elegant to store the state in the
-	 *        changesets structure, or in some place persistent between
-	 *        calls to this function.
-	 */
-	knot_changeset_t *chset = knot_changesets_get_last(*chs);
-	if (state != -1) {
-		dbg_xfrin_detail("State is not -1, deciding...\n");
-		// there should be at least one started changeset right now
-		if (EMPTY_LIST((*chs)->sets)) {
-			ret = KNOT_EMALF;
-			goto cleanup;
-		}
-
-		// a changeset should be created only when there is a SOA
-		assert(chset->soa_from != NULL);
-
-		if (chset->soa_to == NULL) {
-			state = KNOT_CHANGESET_REMOVE;
-		} else {
-			state = KNOT_CHANGESET_ADD;
-		}
-	}
-
-	dbg_xfrin_detail("State before the loop: %d\n", state);
-
-	/*! \todo This may be implemented with much less IFs! */
-
-	while (rr != NULL) {
-		if (!knot_dname_is_sub(rr->owner, xfr->zone->name) &&
-		    !knot_dname_is_equal(rr->owner, xfr->zone->name)) {
-			// out-of-zone domain
-			// take next RR
-			xfrin_take_rr(answer, &rr, &rr_id);
-			continue;
-		}
-
-		switch (state) {
-		case -1:
-			// a SOA is expected
-			// this may be either a start of a changeset or the
-			// last SOA (in case the transfer was empty, but that
-			// is quite weird in fact
-			if (rr->type != KNOT_RRTYPE_SOA) {
-				dbg_xfrin("First RR is not a SOA RR!\n");
-				dbg_xfrin_verb("RR type: %u\n",
-					       rr->type);
-				ret = KNOT_EMALF;
-				goto cleanup;
-			}
-
-			if (knot_soa_serial(&rr->rrs)
-			    == knot_soa_serial(&(*chs)->first_soa->rrs)) {
-
-				/*! \note [TSIG] Check TSIG, we're at the end of
-				 *               transfer.
-				 */
-				ret = xfrin_check_tsig(pkt, xfr, 1);
-
-				// last SOA, discard and end
-				/*! \note [TSIG] If TSIG validates, consider
-				 *        transfer complete. */
-				if (ret == KNOT_EOK) {
-					ret = XFRIN_RES_COMPLETE;
-				}
-
-				return ret;
-			} else {
-				// normal SOA, start new changeset
-				/* Check changesets for maximum count (so they fit into journal). */
-				if ((*chs)->count + 1 > JOURNAL_NCOUNT)
-					ret = KNOT_ESPACE;
-
-				if (ret != KNOT_EOK) {
-					goto cleanup;
-				}
-
-				chset = knot_changesets_create_changeset(*chs);
-				if (chset == NULL) {
-					goto cleanup;
-				}
-				knot_rrset_t *soa = knot_rrset_copy(rr, NULL);
-				if (soa == NULL) {
-					ret = KNOT_ENOMEM;
-					goto cleanup;
-				}
-
-				knot_changeset_add_soa(chset, soa, KNOT_CHANGESET_REMOVE);
-
-				// change state to REMOVE
-				state = KNOT_CHANGESET_REMOVE;
-			}
-			break;
-		case KNOT_CHANGESET_REMOVE:
-			// if the next RR is SOA, store it and change state to
-			// ADD
-			if (rr->type == KNOT_RRTYPE_SOA) {
-				// we should not be here if soa_from is not set
-				assert(chset->soa_from != NULL);
-				knot_rrset_t *soa = knot_rrset_copy(rr, NULL);
-				if (soa == NULL) {
-					ret = KNOT_ENOMEM;
-					goto cleanup;
-				}
-				knot_changeset_add_soa(chset, soa, KNOT_CHANGESET_ADD);
-
-				state = KNOT_CHANGESET_ADD;
-			} else {
-				// just add the RR to the REMOVE part and
-				// continue
-				knot_rrset_t *cpy = knot_rrset_copy(rr, NULL);
-				if (cpy == NULL) {
-					ret = KNOT_ENOMEM;
-					goto cleanup;
-				}
-				ret = knot_changeset_add_rrset(chset, cpy,
-				                               KNOT_CHANGESET_REMOVE);
-				if (ret != KNOT_EOK) {
-					goto cleanup;
-				}
-			}
-			break;
-		case KNOT_CHANGESET_ADD:
-			// if the next RR is SOA change to state -1 and do not
-			// parse next RR
-			if (rr->type == KNOT_RRTYPE_SOA) {
-				log_zone_info("%s Serial %u -> %u.\n",
-					      xfr->msg,
-					      knot_soa_serial(&chset->soa_from->rrs),
-					      knot_soa_serial(&chset->soa_to->rrs));
-				state = -1;
-				continue;
-			} else {
-				// just add the RR to the ADD part and continue
-				knot_rrset_t *cpy = knot_rrset_copy(rr, NULL);
-				if (cpy == NULL) {
-					ret = KNOT_ENOMEM;
-					goto cleanup;
-				}
-				ret = knot_changeset_add_rrset(chset, cpy,
-				                               KNOT_CHANGESET_ADD);
-				if (ret != KNOT_EOK) {
-					goto cleanup;
-				}
-			}
-			break;
-		}
-
-		// take next RR
-		xfrin_take_rr(answer, &rr, &rr_id);
-	}
-
-	/*! \note Check TSIG, we're at the end of packet. It may not be
-	 *        required.
-	 */
-	ret = xfrin_check_tsig(pkt, xfr,
-			       knot_ns_tsig_required(xfr->packet_nr));
-	dbg_xfrin_verb("xfrin_check_tsig() returned %d\n", ret);
-	++xfr->packet_nr;
-
-	/*! \note [TSIG] Cleanup and propagate error if TSIG validation fails.*/
-	if (ret != KNOT_EOK) {
-		goto cleanup;
-	}
-
-	// here no RRs remain in the packet but the transfer is not finished
-	// yet, return EOK
-	return KNOT_EOK;
-
-cleanup:
-	/* We should go here only if some error occured. */
-	assert(ret < 0);
-
-	dbg_xfrin_detail("Cleanup after processing IXFR/IN packet.\n");
-	knot_changesets_free(chs);
-	xfr->data = 0;
-	return ret;
-}
-
-/*----------------------------------------------------------------------------*/
-/* Applying changesets to zone                                                */
-/*----------------------------------------------------------------------------*/
-
-void xfrin_cleanup_successful_update(knot_changesets_t *chgs)
-{
-	if (chgs == NULL) {
-		return;
-	}
-
-	knot_changeset_t *change = NULL;
-	WALK_LIST(change, chgs->sets) {
-		// Delete old RR data
-		rrs_list_clear(&change->old_data, NULL);
-		init_list(&change->old_data);
-		// Keep new RR data
-		ptrlist_free(&change->new_data, NULL);
-		init_list(&change->new_data);
-	};
-}
-
-/*----------------------------------------------------------------------------*/
-
-static int free_additional(zone_node_t **node, void *data)
-{
-	UNUSED(data);
-	if ((*node)->flags & NODE_FLAGS_NONAUTH) {
-		// non-auth nodes have no additionals.
-		return KNOT_EOK;
-	}
-
-	for (uint16_t i = 0; i < (*node)->rrset_count; ++i) {
-		struct rr_data *data = &(*node)->rrs[i];
-		if (data->additional) {
-			free(data->additional);
-			data->additional = NULL;
-		}
-	}
-
-	return KNOT_EOK;
-}
-
-void xfrin_zone_contents_free(zone_contents_t **contents)
-{
-	// free the zone tree, but only the structure
-	// (nodes are already destroyed)
-	dbg_zone("Destroying zone tree.\n");
-	// free additional arrays
-	knot_zone_tree_apply((*contents)->nodes, free_additional, NULL);
-	knot_zone_tree_deep_free(&(*contents)->nodes);
-	dbg_zone("Destroying NSEC3 zone tree.\n");
-	knot_zone_tree_deep_free(&(*contents)->nsec3_nodes);
-
-	knot_nsec3param_free(&(*contents)->nsec3_params);
-
-	free(*contents);
-	*contents = NULL;
-}
-
-/*----------------------------------------------------------------------------*/
-
-static void xfrin_cleanup_failed_update(zone_contents_t **new_contents)
-{
-	if (new_contents == NULL) {
-		return;
-	}
-
-	if (*new_contents != NULL) {
-		// destroy the shallow copy of zone
-		xfrin_zone_contents_free(new_contents);
-	}
-
-}
-
-/*----------------------------------------------------------------------------*/
-
-void xfrin_rollback_update(knot_changesets_t *chgs,
-                           zone_contents_t **new_contents)
-{
-	if (chgs != NULL) {
-		knot_changeset_t *change = NULL;
-		WALK_LIST(change, chgs->sets) {
-			// Delete new RR data
-			rrs_list_clear(&change->new_data, NULL);
-			init_list(&change->new_data);
-			// Keep old RR data
-			ptrlist_free(&change->old_data, NULL);
-			init_list(&change->old_data);
-		};
-	}
-	xfrin_cleanup_failed_update(new_contents);
-}
-
-/*----------------------------------------------------------------------------*/
-
-static int xfrin_replace_rrs_with_copy(zone_node_t *node,
-                                       uint16_t type)
-{
-	// Find data to copy.
-	struct rr_data *data = NULL;
-	for (uint16_t i = 0; i < node->rrset_count; ++i) {
-		if (node->rrs[i].type == type) {
-			data = &node->rrs[i];
-			break;
-		}
-	}
-	assert(data);
-
-	// Create new data.
-	knot_rdataset_t *rrs = &data->rrs;
-	void *copy = malloc(knot_rdataset_size(rrs));
-	if (copy == NULL) {
-		return KNOT_ENOMEM;
-	}
-
-	memcpy(copy, rrs->data, knot_rdataset_size(rrs));
-
-	// Store new data into node RRS.
-	rrs->data = copy;
-
-	return KNOT_EOK;
-}
-
-static void clear_new_rrs(zone_node_t *node, uint16_t type)
-{
-	knot_rdataset_t *new_rrs = node_rdataset(node, type);
-	if (new_rrs) {
-		knot_rdataset_clear(new_rrs, NULL);
-	}
-}
-
-static bool can_remove(const zone_node_t *node, const knot_rrset_t *rr)
-{
-	if (node == NULL) {
-		// Node does not exist, cannot remove anything.
-		return false;
-	}
-	const knot_rdataset_t *node_rrs = node_rdataset(node, rr->type);
-	if (node_rrs == NULL) {
-		// Node does not have this type at all.
-		return false;
-	}
-
-	const bool compare_ttls = false;
-	for (uint16_t i = 0; i < rr->rrs.rr_count; ++i) {
-		knot_rdata_t *rr_cmp = knot_rdataset_at(&rr->rrs, i);
-		if (knot_rdataset_member(node_rrs, rr_cmp, compare_ttls)) {
-			// At least one RR matches.
-			return true;
-		}
-	}
-
-	// Node does have the type, but no RRs match.
-	return false;
-}
-
-static int add_old_data(knot_changeset_t *chset, knot_rdata_t *old_data)
-{
-	if (ptrlist_add(&chset->old_data, old_data, NULL) == NULL) {
-		return KNOT_ENOMEM;
-	}
-
-	return KNOT_EOK;
-}
-
-static int add_new_data(knot_changeset_t *chset, knot_rdata_t *new_data)
-{
-	if (ptrlist_add(&chset->new_data, new_data, NULL) == NULL) {
-		return KNOT_ENOMEM;
-	}
-
-	return KNOT_EOK;
-}
-
-static int remove_rr(zone_node_t *node, const knot_rrset_t *rr,
-                     knot_changeset_t *chset)
-{
-	knot_rrset_t removed_rrset = node_rrset(node, rr->type);
-	knot_rdata_t *old_data = removed_rrset.rrs.data;
-	int ret = xfrin_replace_rrs_with_copy(node, rr->type);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	// Store old data for cleanup.
-	ret = add_old_data(chset, old_data);
-	if (ret != KNOT_EOK) {
-		clear_new_rrs(node, rr->type);
-		return ret;
-	}
-
-	knot_rdataset_t *changed_rrs = node_rdataset(node, rr->type);
-	// Subtract changeset RRS from node RRS.
-	ret = knot_rdataset_subtract(changed_rrs, &rr->rrs, NULL);
-	if (ret != KNOT_EOK) {
-		clear_new_rrs(node, rr->type);
-		return ret;
-	}
-
-	if (changed_rrs->rr_count > 0) {
-		// Subtraction left some data in RRSet, store it for rollback.
-		ret = add_new_data(chset, changed_rrs->data);
-		if (ret != KNOT_EOK) {
-			knot_rdataset_clear(changed_rrs, NULL);
-			return ret;
-		}
-	} else {
-		// RRSet is empty now, remove it from node, all data freed.
-		node_remove_rdataset(node, rr->type);
-	}
-
-	return KNOT_EOK;
-}
-
-static int xfrin_apply_remove(zone_contents_t *contents, knot_changeset_t *chset)
-{
-	knot_rr_ln_t *rr_node = NULL;
-	WALK_LIST(rr_node, chset->remove) {
-		const knot_rrset_t *rr = rr_node->rr;
-
-		// Find node for this owner
-		zone_node_t *node = zone_contents_find_node_for_rr(contents,
-		                                                   rr);
-		if (!can_remove(node, rr)) {
-			// Nothing to remove from, skip.
-			continue;
-		}
-
-		int ret = remove_rr(node, rr, chset);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-	}
-
-	return KNOT_EOK;
-}
-
-static int add_rr(zone_node_t *node, const knot_rrset_t *rr,
-                  knot_changeset_t *chset, bool master)
-{
-	knot_rrset_t changed_rrset = node_rrset(node, rr->type);
-	if (!knot_rrset_empty(&changed_rrset)) {
-		// Modifying existing RRSet.
-		knot_rdata_t *old_data = changed_rrset.rrs.data;
-		int ret = xfrin_replace_rrs_with_copy(node, rr->type);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-
-		// Store old RRS for cleanup.
-		ret = add_old_data(chset, old_data);
-		if (ret != KNOT_EOK) {
-			clear_new_rrs(node, rr->type);
-			return ret;
-		}
-	}
-
-	// Insert new RR to RRSet, data will be copied.
-	int ret = node_add_rrset(node, rr);
-	if (ret == KNOT_EOK || ret == KNOT_ETTL) {
-		// RR added, store for possible rollback.
-		knot_rdataset_t *rrs = node_rdataset(node, rr->type);
-		int data_ret = add_new_data(chset, rrs->data);
-		if (data_ret != KNOT_EOK) {
-			knot_rdataset_clear(rrs, NULL);
-			return data_ret;
-		}
-
-		if (ret == KNOT_ETTL) {
-			// Handle possible TTL errors.
-			log_ttl_error(node, rr);
-			if (!master) {
-				// TTL errors fatal only for master.
-				return KNOT_EOK;
-			}
-		}
-	}
-
-	return ret;
-}
-
-static int xfrin_apply_add(zone_contents_t *contents,
-                           knot_changeset_t *chset, bool master)
-{
-	knot_rr_ln_t *rr_node = NULL;
-	WALK_LIST(rr_node, chset->add) {
-		knot_rrset_t *rr = rr_node->rr;
-
-		// Get or create node with this owner
-		zone_node_t *node = zone_contents_get_node_for_rr(contents, rr);
-		if (node == NULL) {
-			return KNOT_ENOMEM;
-		}
-
-		int ret = add_rr(node, rr, chset, master);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-	}
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-static int xfrin_apply_replace_soa(zone_contents_t *contents,
-                                   knot_changeset_t *chset)
-{
-	assert(chset->soa_from);
-	int ret = remove_rr(contents->apex, chset->soa_from, chset);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	assert(!node_rrtype_exists(contents->apex, KNOT_RRTYPE_SOA));
-
-	return add_rr(contents->apex, chset->soa_to, chset, false);
-}
-
-/*----------------------------------------------------------------------------*/
-
-static int xfrin_apply_changeset(zone_contents_t *contents,
-                                 knot_changeset_t *chset, bool master)
-{
-	/*
-	 * Applies one changeset to the zone. Checks if the changeset may be
-	 * applied (i.e. the origin SOA (soa_from) has the same serial as
-	 * SOA in the zone apex.
-	 */
-
-	dbg_xfrin("APPLYING CHANGESET: from serial %u to serial %u\n",
-		  chset->serial_from, chset->serial_to);
-
-	// check if serial matches
-	const knot_rdataset_t *soa = node_rdataset(contents->apex, KNOT_RRTYPE_SOA);
-	if (soa == NULL || knot_soa_serial(soa) != chset->serial_from) {
-		dbg_xfrin("SOA serials do not match!!\n");
-		return KNOT_EINVAL;
-	}
-
-	int ret = xfrin_apply_remove(contents, chset);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	ret = xfrin_apply_add(contents, chset, master);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	return xfrin_apply_replace_soa(contents, chset);
-}
-
-/*----------------------------------------------------------------------------*/
-
-/*! \brief Wrapper for BIRD lists. Storing: Node. */
-typedef struct knot_node_ln {
-	node_t n; /*!< List node. */
-	zone_node_t *node; /*!< Actual usable data. */
-} knot_node_ln_t;
-
-static int add_node_to_list(zone_node_t *node, list_t *l)
-{
-	assert(node && l);
-	knot_node_ln_t *data = malloc(sizeof(knot_node_ln_t));
-	if (data == NULL) {
-		return KNOT_ENOMEM;
-	}
-	data->node = node;
-	add_head(l, (node_t *)data);
-	return KNOT_EOK;
-}
-
-static int xfrin_mark_empty(zone_node_t **node_p, void *data)
-{
-	assert(node_p && *node_p);
-	zone_node_t *node = *node_p;
-	list_t *l = (list_t *)data;
-	assert(data);
-	if (node->rrset_count == 0 && node->children == 0 &&
-	    !(node->flags & NODE_FLAGS_EMPTY)) {
-		/*!
-		 * Mark this node and all parent nodes that have 0 RRSets and
-		 * no children for removal.
-		 */
-		int ret = add_node_to_list(node, l);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-		node->flags |= NODE_FLAGS_EMPTY;
-		if (node->parent) {
-			if ((node->parent->flags & NODE_FLAGS_WILDCARD_CHILD)
-			    && knot_dname_is_wildcard(node->owner)) {
-				node->parent->flags &= ~NODE_FLAGS_WILDCARD_CHILD;
-			}
-			node->parent->children--;
-			// Recurse using the parent node
-			return xfrin_mark_empty(&node->parent, data);
-		}
-	}
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-static int xfrin_remove_empty_nodes(zone_contents_t *z)
-{
-	dbg_xfrin("Removing empty nodes from zone.\n");
-
-	list_t l;
-	init_list(&l);
-	// walk through the zone and select nodes to be removed
-	int ret = knot_zone_tree_apply(z->nodes,
-	                               xfrin_mark_empty, &l);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	node_t *n = NULL;
-	node_t *nxt = NULL;
-	WALK_LIST_DELSAFE(n, nxt, l) {
-		knot_node_ln_t *list_node = (knot_node_ln_t *)n;
-		ret = zone_contents_remove_node(z, list_node->node->owner);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-		node_free(&list_node->node);
-		free(n);
-	}
-
-	init_list(&l);
-	// Do the same with NSEC3 nodes.
-	ret = knot_zone_tree_apply(z->nsec3_nodes,
-	                           xfrin_mark_empty, &l);
-	if (ret != KNOT_EOK) {
-		return ret;
-	}
-
-	WALK_LIST_DELSAFE(n, nxt, l) {
-		knot_node_ln_t *list_node = (knot_node_ln_t *)n;
-		ret = zone_contents_remove_nsec3_node(z, list_node->node->owner);
-		if (ret != KNOT_EOK) {
-			return ret;
-		}
-		node_free(&list_node->node);
-		free(n);
-	}
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-int xfrin_prepare_zone_copy(zone_contents_t *old_contents, zone_contents_t **new_contents)
-{
-	if (old_contents == NULL || new_contents == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	dbg_xfrin("Preparing zone copy...\n");
-
-	/*
-	 * Ensure that the zone generation is set to 0.
-	 */
-	if (!zone_contents_gen_is_old(old_contents)) {
-		// this would mean that a previous update was not completed
-		// abort
-		dbg_zone("Trying to apply changesets to zone that is "
-				  "being updated. Aborting.\n");
-		return KNOT_EAGAIN;
-	}
-
-	/*
-	 * Create a shallow copy of the zone, so that the structures may be
-	 * updated.
-	 *
-	 * This will create new zone contents structures (normal nodes' tree,
-	 * NSEC3 tree), and copy all nodes.
-	 * The data in the nodes (RRSets) remain the same though.
-	 */
-	zone_contents_t *contents_copy = NULL;
-
-	dbg_xfrin("Copying zone contents.\n");
-	int ret = zone_contents_shallow_copy(old_contents, &contents_copy);
-	if (ret != KNOT_EOK) {
-		dbg_xfrin("Failed to create shallow copy of zone: %s\n",
-			  knot_strerror(ret));
-		return ret;
-	}
-
-	assert(contents_copy->apex != NULL);
-
-	*new_contents = contents_copy;
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-int xfrin_finalize_updated_zone(zone_contents_t *contents_copy,
-                                bool set_nsec3_names)
-{
-	if (contents_copy == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	/*
-	 * Finalize the new zone contents:
-	 * - delete empty nodes
-	 * - parse NSEC3PARAM
-	 * - do adjusting of nodes and RDATA
-	 * - ???
-	 * - PROFIT
-	 */
-
-	/*
-	 * Select and remove empty nodes from zone trees. Do not free them right
-	 * away as they may be referenced by some domain names.
-	 */
-	int ret = xfrin_remove_empty_nodes(contents_copy);
-	if (ret != KNOT_EOK) {
-		dbg_xfrin("Failed to remove empty nodes: %s\n",
-			  knot_strerror(ret));
-		return ret;
-	}
-
-	dbg_xfrin("Adjusting zone contents.\n");
-	if (set_nsec3_names) {
-		ret = zone_contents_adjust_full(contents_copy, NULL, NULL);
-	} else {
-		ret = zone_contents_adjust_pointers(contents_copy);
-	}
-	if (ret != KNOT_EOK) {
-		dbg_xfrin("Failed to finalize zone contents: %s\n",
-			  knot_strerror(ret));
-		return ret;
-	}
-
-	assert(contents_copy->apex != NULL);
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-int xfrin_apply_changesets_directly(zone_contents_t *contents,
-                                    knot_changesets_t *chsets)
-{
-	if (contents == NULL || chsets == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	knot_changeset_t *set = NULL;
-	WALK_LIST(set, chsets->sets) {
-		const bool master = true; // Only DNSSEC changesets are applied directly.
-		int ret = xfrin_apply_changeset(contents, set, master);
-		if (ret != KNOT_EOK) {
-			xfrin_cleanup_successful_update(chsets);
-			return ret;
-		}
-	}
-
-	int ret = xfrin_finalize_updated_zone(contents, true);
-
-	/*
-	 * HACK: Cleanup for successful update is used for both success and fail
-	 * when modifying the zone directly, will fix in new zone API.
-	 */
-	xfrin_cleanup_successful_update(chsets);
-	return ret;
-}
-
-/*----------------------------------------------------------------------------*/
-
-int xfrin_apply_changesets(zone_t *zone,
-                           knot_changesets_t *chsets,
-                           zone_contents_t **new_contents)
-{
-	if (zone == NULL || chsets == NULL || EMPTY_LIST(chsets->sets)
-	    || new_contents == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	zone_contents_t *old_contents = zone->contents;
-	if (!old_contents) {
-		dbg_xfrin("Cannot apply changesets to empty zone.\n");
-		return KNOT_EINVAL;
-	}
-
-	dbg_xfrin("Applying changesets to zone...\n");
-
-	dbg_xfrin_verb("Creating shallow copy of the zone...\n");
-	zone_contents_t *contents_copy = NULL;
-	int ret = xfrin_prepare_zone_copy(old_contents, &contents_copy);
-	if (ret != KNOT_EOK) {
-		dbg_xfrin("Failed to prepare zone copy: %s\n",
-			  knot_strerror(ret));
-		return ret;
-	}
-
-	/*
-	 * Apply the changesets.
-	 */
-	dbg_xfrin("Applying changesets.\n");
-	dbg_xfrin_verb("Old contents apex: %p, new apex: %p\n",
-		       old_contents->apex, contents_copy->apex);
-	knot_changeset_t *set = NULL;
-	const bool master = (zone_master(zone) == NULL);
-	WALK_LIST(set, chsets->sets) {
-		ret = xfrin_apply_changeset(contents_copy, set, master);
-		if (ret != KNOT_EOK) {
-			xfrin_rollback_update(chsets, &contents_copy);
-			dbg_xfrin("Failed to apply changesets to zone: "
-				  "%s\n", knot_strerror(ret));
-			return ret;
-		}
-	}
-
-	assert(contents_copy->apex != NULL);
-
-	/*!
-	 * \todo Test failure of IXFR.
-	 */
-
-	dbg_xfrin_verb("Finalizing updated zone...\n");
-	ret = xfrin_finalize_updated_zone(contents_copy, true);
-	if (ret != KNOT_EOK) {
-		dbg_xfrin("Failed to finalize updated zone: %s\n",
-			  knot_strerror(ret));
-		xfrin_rollback_update(chsets, &contents_copy);
-		return ret;
-	}
-
-	*new_contents = contents_copy;
-
-	return KNOT_EOK;
-}
-
-/*----------------------------------------------------------------------------*/
-
-zone_contents_t *xfrin_switch_zone(zone_t *zone, zone_contents_t *new_contents)
-{
-	if (zone == NULL || new_contents == NULL) {
-		return NULL;
-	}
-
-	dbg_xfrin("Switching zone contents.\n");
-	dbg_xfrin_verb("Old contents: %p, apex: %p, new apex: %p\n",
-		       zone->contents, (zone->contents)
-		       ? zone->contents->apex : NULL, new_contents->apex);
-
-	zone_contents_t *old =
-		zone_switch_contents(zone, new_contents);
-
-	dbg_xfrin_verb("Old contents: %p, apex: %p, new apex: %p\n",
-		       old, (old) ? old->apex : NULL, new_contents->apex);
-
-	// set generation to old, so that the flags may be used in next transfer
-	// and we do not search for new nodes anymore
-	zone_contents_set_gen_old(new_contents);
-
-	// wait for readers to finish
-	dbg_xfrin_verb("Waiting for readers to finish...\n");
-	synchronize_rcu();
-
-	return old;
-}
diff --git a/src/knot/updates/xfr-in.h b/src/knot/updates/xfr-in.h
deleted file mode 100644
index 8ccd9471f045316cace5a14aeea79e93aac9dc6f..0000000000000000000000000000000000000000
--- a/src/knot/updates/xfr-in.h
+++ /dev/null
@@ -1,163 +0,0 @@
-/*!
- * \file xfr-in.h
- *
- * \author Lubos Slovak <lubos.slovak@nic.cz>
- *
- * \brief XFR client API.
- *
- * \addtogroup xfr
- * @{
- */
-/*  Copyright (C) 2011 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz>
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License
-    along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-#ifndef _KNOT_XFR_IN_H_
-#define _KNOT_XFR_IN_H_
-
-#include <stdint.h>
-#include <string.h>
-
-#include "libknot/dname.h"
-#include "knot/zone/zone.h"
-#include "libknot/packet/pkt.h"
-#include "knot/server/xfr-handler.h"
-#include "knot/updates/changesets.h"
-
-struct xfr_proc;
-
-/*----------------------------------------------------------------------------*/
-
-typedef enum xfrin_transfer_result {
-	XFRIN_RES_COMPLETE = 1,
-	XFRIN_RES_SOA_ONLY = 2,
-	XFRIN_RES_FALLBACK = 3
-} xfrin_transfer_result_t;
-
-/*----------------------------------------------------------------------------*/
-
-/*!
- * \brief Checks if a zone transfer is required by comparing the zone's SOA with
- *        the one received from master server.
- *
- * \param zone Zone to check.
- * \param soa_response Response to SOA query received from master server.
- *
- * \retval < 0 if an error occured.
- * \retval 1 if the transfer is needed.
- * \retval 0 if the transfer is not needed.
- */
-int xfrin_transfer_needed(const zone_contents_t *zone,
-                          knot_pkt_t *soa_response);
-
-/*!
- * \brief Processes one incoming packet of AXFR transfer by updating the given
- *        zone.
- *
- * \param pkt Incoming packet in wire format.
- * \param size Size of the packet in bytes.
- * \param zone Zone being built. If there is no such zone (i.e. this is the
- *             first packet, \a *zone may be set to NULL, in which case a new
- *             zone structure is created).
- *
- * \retval KNOT_EOK
- *
- * \todo Refactor!!!
- */
-int xfrin_process_axfr_packet(knot_pkt_t *pkt, struct xfr_proc *proc);
-
-/*!
- * \brief Destroys the whole changesets structure.
- *
- * Frees all RRSets present in the changesets and all their data. Also frees
- * the changesets structure and sets the parameter to NULL.
- *
- * \param changesets Changesets to destroy.
- */
-void xfrin_free_changesets(knot_changesets_t **changesets);
-
-/*!
- * \brief Parses IXFR reply packet and fills in the changesets structure.
- *
- * \param pkt Packet containing the IXFR reply in wire format.
- * \param size Size of the packet in bytes.
- * \param changesets Changesets to be filled in.
- *
- * \retval KNOT_EOK
- * \retval KNOT_EINVAL
- * \retval KNOT_EMALF
- * \retval KNOT_ENOMEM
- */
-int xfrin_process_ixfr_packet(knot_pkt_t *pkt, knot_ns_xfr_t *xfr);
-
-/*!
- * \brief Applies changesets *with* zone shallow copy.
- *
- * \param zone          Zone to be updated.
- * \param chsets        Changes to be made.
- * \param new_contents  New zone will be returned using this arg.
- * \return KNOT_E*
- */
-int xfrin_apply_changesets(zone_t *zone,
-                           knot_changesets_t *chsets,
-                           zone_contents_t **new_contents);
-
-/*!
- * \brief Applies changesets directly to the zone, without copying it.
- *
- * \param contents Zone contents to apply the changesets to. Will be modified.
- * \param chsets   Changesets to be applied to the zone.
- *
- * \retval KNOT_EOK if successful.
- * \retval KNOT_EINVAL if given one of the arguments is NULL.
- * \return Other error code if the application went wrong.
- */
-int xfrin_apply_changesets_directly(zone_contents_t *contents,
-                                    knot_changesets_t *chsets);
-
-int xfrin_prepare_zone_copy(zone_contents_t *old_contents,
-                            zone_contents_t **new_contents);
-
-/*!
- * \brief Sets pointers and NSEC3 nodes after signing/DDNS.
- * \param contents_copy    Contents to be updated.
- * \param set_nsec3_names  Set to true if NSEC3 hashes should be set.
- * \return KNOT_E*
- */
-int xfrin_finalize_updated_zone(zone_contents_t *contents_copy,
-                                bool set_nsec3_names);
-
-zone_contents_t *xfrin_switch_zone(zone_t *zone, zone_contents_t *new_contents);
-
-void xfrin_rollback_update(knot_changesets_t *chgs,
-                           zone_contents_t **new_contents);
-
-int xfrin_copy_rrset(zone_node_t *node, uint16_t type,
-                     knot_rrset_t **rrset);
-
-int xfrin_copy_old_rrset(knot_rrset_t *old, knot_rrset_t **copy);
-
-int xfrin_replace_rrset_in_node(zone_node_t *node,
-                                knot_rrset_t *rrset_new,
-                                zone_contents_t *contents);
-
-void xfrin_cleanup_successful_update(knot_changesets_t *chgs);
-
-/* @note Exported because of update.c */
-void xfrin_zone_contents_free(zone_contents_t **contents);
-
-#endif /* _KNOTXFR_IN_H_ */
-
-/*! @} */
diff --git a/src/knot/zone/contents.c b/src/knot/zone/contents.c
index e897d6374cab1acbcaf2f079be67bd697e05ec28..7399e8e6914b2bebc05c74830e80531243bbbfca 100644
--- a/src/knot/zone/contents.c
+++ b/src/knot/zone/contents.c
@@ -49,13 +49,6 @@ typedef struct {
 
 /*----------------------------------------------------------------------------*/
 
-const uint8_t KNOT_ZONE_FLAGS_GEN_OLD  = 0;            /* xxxxxx00 */
-const uint8_t KNOT_ZONE_FLAGS_GEN_NEW  = 1 << 0;       /* xxxxxx01 */
-const uint8_t KNOT_ZONE_FLAGS_GEN_FIN  = 1 << 1;       /* xxxxxx10 */
-const uint8_t KNOT_ZONE_FLAGS_GEN_MASK = 3;            /* 00000011 */
-
-/*----------------------------------------------------------------------------*/
-
 static int tree_apply_cb(zone_node_t **node, void *data)
 {
 	if (node == NULL || data == NULL) {
@@ -418,7 +411,6 @@ zone_contents_t *zone_contents_new(const knot_dname_t *apex_name)
 	}
 
 	memset(contents, 0, sizeof(zone_contents_t));
-	contents->node_count = 1;
 	contents->apex = node_new(apex_name);
 	if (contents->apex == NULL) {
 		goto cleanup;
@@ -445,36 +437,6 @@ cleanup:
 
 /*----------------------------------------------------------------------------*/
 
-int zone_contents_gen_is_old(const zone_contents_t *contents)
-{
-	return ((contents->flags & KNOT_ZONE_FLAGS_GEN_MASK)
-		== KNOT_ZONE_FLAGS_GEN_OLD);
-}
-
-/*----------------------------------------------------------------------------*/
-
-int zone_contents_gen_is_new(const zone_contents_t *contents)
-{
-	return ((contents->flags & KNOT_ZONE_FLAGS_GEN_MASK)
-		== KNOT_ZONE_FLAGS_GEN_NEW);
-}
-
-/*----------------------------------------------------------------------------*/
-
-void zone_contents_set_gen_old(zone_contents_t *contents)
-{
-	contents->flags &= ~KNOT_ZONE_FLAGS_GEN_MASK;
-	contents->flags |= KNOT_ZONE_FLAGS_GEN_OLD;
-}
-
-/*----------------------------------------------------------------------------*/
-
-void zone_contents_set_gen_new(zone_contents_t *contents)
-{
-	contents->flags &= ~KNOT_ZONE_FLAGS_GEN_MASK;
-	contents->flags |= KNOT_ZONE_FLAGS_GEN_NEW;
-}
-
 static zone_node_t *zone_contents_get_node(const zone_contents_t *zone,
                                            const knot_dname_t *name)
 {
@@ -513,8 +475,6 @@ static int zone_contents_add_node(zone_contents_t *zone, zone_node_t *node,
 		return ret;
 	}
 
-	++zone->node_count;
-
 	if (!create_parents) {
 		return KNOT_EOK;
 	}
@@ -561,8 +521,6 @@ static int zone_contents_add_node(zone_contents_t *zone, zone_node_t *node,
 				next_node->flags |= NODE_FLAGS_WILDCARD_CHILD;
 			}
 
-			++zone->node_count;
-
 			dbg_zone_detail("Next parent.\n");
 			node = next_node;
 			parent = knot_wire_next_label(parent, NULL);
@@ -1311,9 +1269,6 @@ int zone_contents_shallow_copy(const zone_contents_t *from, zone_contents_t **to
 		return KNOT_ENOMEM;
 	}
 
-	contents->flags = from->flags;
-	zone_contents_set_gen_new(contents);
-
 	int ret = recreate_normal_tree(from, contents);
 	if (ret != KNOT_EOK) {
 		knot_zone_tree_free(&contents->nodes);
diff --git a/src/knot/zone/contents.h b/src/knot/zone/contents.h
index 235e54c56299827418650a8a1a2c1527440b58fe..a521c9f65c5c7d3eea99d968c76f565586117319 100644
--- a/src/knot/zone/contents.h
+++ b/src/knot/zone/contents.h
@@ -47,35 +47,8 @@ typedef struct zone_contents_t {
 	knot_zone_tree_t *nsec3_nodes;
 
 	knot_nsec3_params_t nsec3_params;
-
-	/*!
-	 * \todo Unify the use of this field - authoritative nodes vs. all.
-	 */
-	size_t node_count;
-
-	/*! \brief Various flags
-	 *
-	 * Two rightmost bits denote zone contents generation.
-	 *
-	 * Possible values:
-	 * - 00 - Original version of the zone. Old nodes should be used.
-	 * - 01 - New (updated) zone. New nodes should be used.
-	 * - 10 - New (updated) zone, but exactly the stored nodes should be
-	 *        used, no matter their generation.
-	 *
-	 * The third bit denotes whether ANY queries are enabled or disabled:
-	 * - 1xx - ANY queries disabled
-	 * - 0xx - ANY queries enabled
-	 */
-	uint8_t flags;
 } zone_contents_t;
 
-/*!< \brief Helper linked list list for CNAME loop checking */
-typedef struct cname_chain {
-	const zone_node_t *node;
-	struct cname_chain *next;
-} cname_chain_t;
-
 /*!
  * \brief Signature of callback for zone contents apply functions.
  */
@@ -85,12 +58,6 @@ typedef int (*zone_contents_apply_cb_t)(zone_node_t *node, void *data);
 
 zone_contents_t *zone_contents_new(const knot_dname_t *apex_name);
 
-int zone_contents_gen_is_old(const zone_contents_t *contents);
-int zone_contents_gen_is_new(const zone_contents_t *contents);
-
-void zone_contents_set_gen_old(zone_contents_t *contents);
-void zone_contents_set_gen_new(zone_contents_t *contents);
-
 int zone_contents_add_rr(zone_contents_t *z, const knot_rrset_t *rr, zone_node_t **n);
 
 int zone_contents_remove_node(zone_contents_t *contents, const knot_dname_t *owner);
diff --git a/src/knot/zone/events.c b/src/knot/zone/events.c
index 2e0e35d38e772f9a82ed383c4c9c1c9bf80ec7d0..97afbae4e4770d063eff4369001d9a593f8dba32 100644
--- a/src/knot/zone/events.c
+++ b/src/knot/zone/events.c
@@ -297,9 +297,12 @@ static int event_xfer(zone_t *zone)
 	int ret = zone_query_execute(zone, pkt_type, master);
 
 	/* IXFR failed, revert to AXFR. */
-	if (pkt_type == KNOT_RRTYPE_IXFR && ret != KNOT_EOK) {
+	if (pkt_type == KNOT_QUERY_IXFR && ret != KNOT_EOK) {
+		ZONE_QUERY_LOG(LOG_WARNING, zone, master, "IXFR", "Fallback to AXFR");
 		zone->flags |= ZONE_FORCE_AXFR;
-		return event_xfer(zone);
+		ret = event_xfer(zone);
+		zone->flags &= ~ZONE_FORCE_AXFR;
+		return ret;
 	}
 
 	if (zone_contents_is_empty(zone->contents)) {
@@ -419,12 +422,12 @@ static int event_dnssec(zone_t *zone)
 	assert(zone);
 	fprintf(stderr, "DNSSEC of '%s'\n", zone->conf->name);
 
-	knot_changesets_t *chs = knot_changesets_create(1);
+	changesets_t *chs = changesets_create(1);
 	if (chs == NULL) {
 		return KNOT_ENOMEM;
 	}
 
-	knot_changeset_t *ch = knot_changesets_get_last(chs);
+	changeset_t *ch = changesets_get_last(chs);
 	assert(ch);
 
 	int ret = KNOT_ERROR;
@@ -454,10 +457,8 @@ static int event_dnssec(zone_t *zone)
 		goto done;
 	}
 
-	if (!knot_changesets_empty(chs)) {
-		zone_contents_t *new_c = NULL;
-		ret = zone_change_apply_and_store(chs, zone, &new_c, "DNSSEC");
-		chs = NULL; // freed by zone_change_apply_and_store()
+	if (!changesets_empty(chs)) {
+		ret = zone_change_apply_and_store(&chs, zone, "DNSSEC", NULL);
 		if (ret != KNOT_EOK) {
 			log_zone_error("%s Could not sign zone (%s).\n",
 				       msgpref, knot_strerror(ret));
@@ -475,7 +476,7 @@ static int event_dnssec(zone_t *zone)
 	}
 
 done:
-	knot_changesets_free(&chs);
+	changesets_free(&chs, NULL);
 	free(msgpref);
 	return ret;
 }
diff --git a/src/knot/zone/zone-diff.c b/src/knot/zone/zone-diff.c
index c21c8d97a8d1188450da93c3e9a64ba3c98428ba..266c559b1d30881f74ec8562d1168fa85814f95a 100644
--- a/src/knot/zone/zone-diff.c
+++ b/src/knot/zone/zone-diff.c
@@ -27,17 +27,17 @@
 
 struct zone_diff_param {
 	knot_zone_tree_t *nodes;
-	knot_changeset_t *changeset;
+	changeset_t *changeset;
 };
 
 // forward declaration
 static int knot_zone_diff_rdata(const knot_rrset_t *rrset1,
                                 const knot_rrset_t *rrset2,
-                                knot_changeset_t *changeset);
+                                changeset_t *changeset);
 
 static int knot_zone_diff_load_soas(const zone_contents_t *zone1,
                                     const zone_contents_t *zone2,
-                                    knot_changeset_t *changeset)
+                                    changeset_t *changeset)
 {
 	if (zone1 == NULL || zone2 == NULL || changeset == NULL) {
 		return KNOT_EINVAL;
@@ -94,7 +94,7 @@ static int knot_zone_diff_load_soas(const zone_contents_t *zone1,
 
 /*!< \todo Only use add or remove function, not both as they are the same. */
 /*!< \todo Also, this might be all handled by function in changesets.h!!! */
-static int knot_zone_diff_changeset_add_rrset(knot_changeset_t *changeset,
+static int knot_zone_diff_changeset_add_rrset(changeset_t *changeset,
                                               const knot_rrset_t *rrset)
 {
 	/* Remove all RRs of the RRSet. */
@@ -114,8 +114,8 @@ static int knot_zone_diff_changeset_add_rrset(knot_changeset_t *changeset,
 		return KNOT_ENOMEM;
 	}
 
-	int ret = knot_changeset_add_rrset(changeset, rrset_copy,
-	                                   KNOT_CHANGESET_ADD);
+	int ret = changeset_add_rrset(changeset, rrset_copy,
+	                                   CHANGESET_ADD);
 	if (ret != KNOT_EOK) {
 		/* We have to free the copy now! */
 		knot_rrset_free(&rrset_copy, NULL);
@@ -127,7 +127,7 @@ static int knot_zone_diff_changeset_add_rrset(knot_changeset_t *changeset,
 	return KNOT_EOK;
 }
 
-static int knot_zone_diff_changeset_remove_rrset(knot_changeset_t *changeset,
+static int knot_zone_diff_changeset_remove_rrset(changeset_t *changeset,
                                                  const knot_rrset_t *rrset)
 {
 	/* Remove all RRs of the RRSet. */
@@ -152,8 +152,8 @@ static int knot_zone_diff_changeset_remove_rrset(knot_changeset_t *changeset,
 		return KNOT_ENOMEM;
 	}
 
-	int ret = knot_changeset_add_rrset(changeset, rrset_copy,
-	                                   KNOT_CHANGESET_REMOVE);
+	int ret = changeset_add_rrset(changeset, rrset_copy,
+	                                   CHANGESET_REMOVE);
 	if (ret != KNOT_EOK) {
 		/* We have to free the copy now. */
 		knot_rrset_free(&rrset_copy, NULL);
@@ -166,7 +166,7 @@ static int knot_zone_diff_changeset_remove_rrset(knot_changeset_t *changeset,
 }
 
 static int knot_zone_diff_add_node(const zone_node_t *node,
-                                   knot_changeset_t *changeset)
+                                   changeset_t *changeset)
 {
 	/* Add all rrsets from node. */
 	for (uint i = 0; i < node->rrset_count; i++) {
@@ -183,7 +183,7 @@ static int knot_zone_diff_add_node(const zone_node_t *node,
 	return KNOT_EOK;
 }
 
-static int knot_zone_diff_remove_node(knot_changeset_t *changeset,
+static int knot_zone_diff_remove_node(changeset_t *changeset,
                                                 const zone_node_t *node)
 {
 	/* Remove all the RRSets of the node. */
@@ -262,7 +262,7 @@ static int knot_zone_diff_rdata_return_changes(const knot_rrset_t *rrset1,
 
 static int knot_zone_diff_rdata(const knot_rrset_t *rrset1,
                                 const knot_rrset_t *rrset2,
-                                knot_changeset_t *changeset)
+                                changeset_t *changeset)
 {
 	if ((changeset == NULL) || (rrset1 == NULL && rrset2 == NULL)) {
 		dbg_zonediff("zone_diff: diff_rdata: NULL arguments.\n");
@@ -325,7 +325,7 @@ static int knot_zone_diff_rdata(const knot_rrset_t *rrset1,
 
 static int knot_zone_diff_rrsets(const knot_rrset_t *rrset1,
                                  const knot_rrset_t *rrset2,
-                                 knot_changeset_t *changeset)
+                                 changeset_t *changeset)
 {
 	/* RRs (=rdata) have to be cross-compared, unfortunalely. */
 	return knot_zone_diff_rdata(rrset1, rrset2, changeset);
@@ -502,7 +502,7 @@ static int knot_zone_diff_add_new_nodes(zone_node_t **node_ptr, void *data)
 
 static int knot_zone_diff_load_trees(knot_zone_tree_t *nodes1,
 				     knot_zone_tree_t *nodes2,
-				     knot_changeset_t *changeset)
+				     changeset_t *changeset)
 {
 	assert(changeset);
 
@@ -526,7 +526,7 @@ static int knot_zone_diff_load_trees(knot_zone_tree_t *nodes1,
 
 static int knot_zone_diff_load_content(const zone_contents_t *zone1,
                                        const zone_contents_t *zone2,
-                                       knot_changeset_t *changeset)
+                                       changeset_t *changeset)
 {
 	int result;
 
@@ -542,7 +542,7 @@ static int knot_zone_diff_load_content(const zone_contents_t *zone1,
 
 static int zone_contents_diff(const zone_contents_t *zone1,
                               const zone_contents_t *zone2,
-                              knot_changeset_t *changeset)
+                              changeset_t *changeset)
 {
 	if (zone1 == NULL || zone2 == NULL) {
 		return KNOT_EINVAL;
@@ -558,7 +558,7 @@ static int zone_contents_diff(const zone_contents_t *zone1,
 
 int zone_contents_create_diff(const zone_contents_t *z1,
                                    const zone_contents_t *z2,
-                                   knot_changeset_t *changeset)
+                                   changeset_t *changeset)
 {
 	int ret = zone_contents_diff(z1, z2, changeset);
 	if (ret != KNOT_EOK) {
@@ -573,7 +573,7 @@ int zone_contents_create_diff(const zone_contents_t *z1,
 }
 
 int knot_zone_tree_add_diff(knot_zone_tree_t *t1, knot_zone_tree_t *t2,
-                            knot_changeset_t *changeset)
+                            changeset_t *changeset)
 {
 	if (!changeset) {
 		return KNOT_EINVAL;
diff --git a/src/knot/zone/zone-diff.h b/src/knot/zone/zone-diff.h
index 051efccf1f13672fa8fe1ad5b52f6ae9f68357cb..07d1589b18f2ad09eb5b6128f73590139abc117a 100644
--- a/src/knot/zone/zone-diff.h
+++ b/src/knot/zone/zone-diff.h
@@ -27,12 +27,12 @@
  * */
 int zone_contents_create_diff(const zone_contents_t *z1,
                                    const zone_contents_t *z2,
-                                   knot_changeset_t *changeset);
+                                   changeset_t *changeset);
 
 /*!
  * \brief Add diff between two zone trees into the changeset.
  */
 int knot_zone_tree_add_diff(knot_zone_tree_t *t1, knot_zone_tree_t *t2,
-                            knot_changeset_t *changeset);
+                            changeset_t *changeset);
 
 #endif // _KNOT_ZONE_DIFF_H_
diff --git a/src/knot/zone/zone-load.c b/src/knot/zone/zone-load.c
index 4e787cd21f4aae076dbaf239ac4da9dda98b0cd5..9969da8968e526f227bd48e2a2e917c211f94894 100644
--- a/src/knot/zone/zone-load.c
+++ b/src/knot/zone/zone-load.c
@@ -21,7 +21,7 @@
 #include "knot/zone/contents.h"
 #include "knot/zone/zonefile.h"
 #include "knot/dnssec/zone-events.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "libknot/rdata.h"
 
 zone_contents_t *zone_load_contents(conf_zone_t *conf)
@@ -91,7 +91,7 @@ int zone_load_journal(zone_contents_t *contents, conf_zone_t *conf)
 	uint32_t serial = zone_contents_serial(contents);
 
 	/* Load all pending changesets. */
-	knot_changesets_t* chsets = knot_changesets_create(0);
+	changesets_t* chsets = changesets_create(0);
 	if (chsets == NULL) {
 		return KNOT_ERROR;
 	}
@@ -99,7 +99,7 @@ int zone_load_journal(zone_contents_t *contents, conf_zone_t *conf)
 	/*! \todo Check what should be the upper bound. */
 	int ret = journal_load_changesets(conf->ixfr_db, chsets, serial, serial - 1);
 	if ((ret != KNOT_EOK && ret != KNOT_ERANGE) || EMPTY_LIST(chsets->sets)) {
-		knot_changesets_free(&chsets);
+		changesets_free(&chsets, NULL);
 		/* Absence of records is not an error. */
 		if (ret == KNOT_ENOENT) {
 			return KNOT_EOK;
@@ -109,13 +109,13 @@ int zone_load_journal(zone_contents_t *contents, conf_zone_t *conf)
 	}
 
 	/* Apply changesets. */
-	ret = xfrin_apply_changesets_directly(contents,  chsets);
+	ret = apply_changesets_directly(contents,  chsets);
 	log_zone_info("Zone '%s' serial %u -> %u: %s\n",
 	              conf->name,
 	              serial, zone_contents_serial(contents),
 	              knot_strerror(ret));
 
-	knot_changesets_free(&chsets);
+	changesets_free(&chsets, NULL);
 	return ret;
 }
 
@@ -127,8 +127,8 @@ int zone_load_post(zone_contents_t *new_contents, zone_t *zone)
 
 	int ret = KNOT_EOK;
 	const conf_zone_t *conf = zone->conf;
-	knot_changeset_t  *change = NULL;
-	knot_changesets_t *chset = knot_changesets_create(0);
+	changeset_t  *change = NULL;
+	changesets_t *chset = changesets_create(0);
 	if (chset == NULL) {
 		return KNOT_ENOMEM;
 	}
@@ -137,7 +137,7 @@ int zone_load_post(zone_contents_t *new_contents, zone_t *zone)
 	if (conf->dnssec_enable) {
 		change = zone_change_prepare(chset);
 		if (change == NULL) {
-			knot_changesets_free(&chset);
+			changesets_free(&chset, NULL);
 			return KNOT_ENOMEM;
 		}
 
@@ -153,13 +153,13 @@ int zone_load_post(zone_contents_t *new_contents, zone_t *zone)
 		/* Commit existing zone change and prepare new. */
 		int ret = zone_change_commit(new_contents, chset);
 		if (ret != KNOT_EOK) {
-			knot_changesets_free(&chset);
+			changesets_free(&chset, NULL);
 			return ret;
 		} else {
-			knot_changesets_clear(chset);
+			changesets_clear(chset, NULL);
 			change = zone_change_prepare(chset);
 			if (change == NULL) {
-				knot_changesets_free(&chset);
+				changesets_free(&chset, NULL);
 				return KNOT_ENOMEM;
 			}
 		}
@@ -182,7 +182,7 @@ int zone_load_post(zone_contents_t *new_contents, zone_t *zone)
 	if (ret == KNOT_EOK) {
 		ret = zone_change_commit(new_contents, chset);
 		if (ret != KNOT_EOK) {
-			knot_changesets_free(&chset);
+			changesets_free(&chset, NULL);
 			return ret;
 		}
 
@@ -191,7 +191,7 @@ int zone_load_post(zone_contents_t *new_contents, zone_t *zone)
 	}
 
 	/* Free changesets and return. */
-	knot_changesets_free(&chset);
+	changesets_free(&chset, NULL);
 	return ret;
 }
 
diff --git a/src/knot/zone/zone.c b/src/knot/zone/zone.c
index fee200dac7dfb544848ee4a73914174b4008c2f1..c7f202c3f101cd0f14f1f5e845c03b566f9741fb 100644
--- a/src/knot/zone/zone.c
+++ b/src/knot/zone/zone.c
@@ -26,12 +26,13 @@
 #include "knot/zone/zone.h"
 #include "knot/zone/zonefile.h"
 #include "knot/zone/contents.h"
-#include "knot/updates/xfr-in.h"
+#include "knot/updates/apply.h"
 #include "knot/nameserver/requestor.h"
 #include "libknot/common.h"
 #include "libknot/dname.h"
 #include "libknot/dnssec/random.h"
 #include "libknot/util/utils.h"
+#include "libknot/rdata/soa.h"
 
 /*!
  * \brief Set ACL list from configuration.
@@ -145,24 +146,24 @@ void zone_free(zone_t **zone_ptr)
 	*zone_ptr = NULL;
 }
 
-knot_changeset_t *zone_change_prepare(knot_changesets_t *chset)
+changeset_t *zone_change_prepare(changesets_t *chset)
 {
-	return knot_changesets_create_changeset(chset);
+	return changesets_create_changeset(chset);
 }
 
-int zone_change_commit(zone_contents_t *contents, knot_changesets_t *chset)
+int zone_change_commit(zone_contents_t *contents, changesets_t *chset)
 {
 	assert(contents);
 
-	if (knot_changesets_empty(chset)) {
+	if (changesets_empty(chset)) {
 		return KNOT_EOK;
 	}
 
 	/* Apply DNSSEC changeset to the new zone. */
-	return xfrin_apply_changesets_directly(contents, chset);
+	return apply_changesets_directly(contents, chset);
 }
 
-int zone_change_store(zone_t *zone, knot_changesets_t *chset)
+int zone_change_store(zone_t *zone, changesets_t *chset)
 {
 	assert(zone);
 	assert(chset);
@@ -186,39 +187,41 @@ int zone_change_store(zone_t *zone, knot_changesets_t *chset)
 }
 
 /*! \note @mvavrusa Moved from zones.c, this needs a common API. */
-int zone_change_apply_and_store(knot_changesets_t *chs,
+int zone_change_apply_and_store(changesets_t **chs,
                                 zone_t *zone,
-                                zone_contents_t **new_contents,
-                                const char *msgpref)
+                                const char *msgpref,
+                                mm_ctx_t *rr_mm)
 {
 	int ret = KNOT_EOK;
 
 	/* Now, try to apply the changesets to the zone. */
-	ret = xfrin_apply_changesets(zone, chs, new_contents);
+	zone_contents_t *new_contents;
+	ret = apply_changesets(zone, *chs, &new_contents);
 	if (ret != KNOT_EOK) {
 		log_zone_error("%s Failed to apply changesets.\n", msgpref);
 		/* Free changesets, but not the data. */
-		knot_changesets_free(&chs);
+		changesets_free(chs, rr_mm);
 		return ret;  // propagate the error above
 	}
 
 	/* Write changes to journal if all went well. */
-	ret = zone_change_store(zone, chs);
+	ret = zone_change_store(zone, *chs);
 	if (ret != KNOT_EOK) {
 		log_zone_error("%s Failed to store changesets.\n", msgpref);
-		xfrin_rollback_update(chs, new_contents);
+		update_rollback(*chs, &new_contents);
 		/* Free changesets, but not the data. */
-		knot_changesets_free(&chs);
+		changesets_free(chs, rr_mm);
 		return ret;  // propagate the error above
 	}
 
 	/* Switch zone contents. */
-	zone_contents_t *old_contents = xfrin_switch_zone(zone, *new_contents);
-	xfrin_zone_contents_free(&old_contents);
+	zone_contents_t *old_contents = zone_switch_contents(zone, new_contents);
+	synchronize_rcu();
+	update_free_old_zone(&old_contents);
 
 	/* Free changesets, but not the data. */
-	xfrin_cleanup_successful_update(chs);
-	knot_changesets_free(&chs);
+	update_cleanup(*chs);
+	changesets_free(chs, rr_mm);
 	assert(ret == KNOT_EOK);
 	return KNOT_EOK;
 }
@@ -353,3 +356,16 @@ struct request_data *zone_update_dequeue(zone_t *zone)
 
 	return ret;
 }
+
+bool zone_transfer_needed(const zone_t *zone, const knot_pkt_t *pkt)
+{
+	const knot_pktsection_t *answer = knot_pkt_section(pkt, KNOT_ANSWER);
+	const knot_rrset_t soa = answer->rr[0];
+	if (soa.type != KNOT_RRTYPE_SOA) {
+		return false;
+	}
+
+	return knot_serial_compare(zone_contents_serial(zone->contents),
+	                           knot_soa_serial(&soa.rrs)) < 0;
+}
+
diff --git a/src/knot/zone/zone.h b/src/knot/zone/zone.h
index bba1eb220ee5717e7682973937ef5691f0070cca..0724fa52bb15c8caaad5dfc380a49c37acc303f8 100644
--- a/src/knot/zone/zone.h
+++ b/src/knot/zone/zone.h
@@ -112,14 +112,14 @@ zone_t *zone_new(conf_zone_t *conf);
 void zone_free(zone_t **zone_ptr);
 
 /*! \note Zone change API, subject to change. */
-knot_changeset_t *zone_change_prepare(knot_changesets_t *chset);
-int zone_change_commit(zone_contents_t *contents, knot_changesets_t *chset);
-int zone_change_store(zone_t *zone, knot_changesets_t *chset);
+changeset_t *zone_change_prepare(changesets_t *chset);
+int zone_change_commit(zone_contents_t *contents, changesets_t *chset);
+int zone_change_store(zone_t *zone, changesets_t *chset);
 /*! \note @mvavrusa Moved from zones.c, this needs a common API. */
-int zone_change_apply_and_store(knot_changesets_t *chs,
+int zone_change_apply_and_store(changesets_t **chs,
                                 zone_t *zone,
-                                zone_contents_t **new_contents,
-                                const char *msgpref);
+                                const char *msgpref,
+                                mm_ctx_t *rr_mm);
 /*!
  * \brief Atomically switch the content of the zone.
  */
@@ -141,4 +141,8 @@ int zone_update_enqueue(zone_t *zone, knot_pkt_t *pkt, struct process_query_para
 /*! \brief Dequeue UPDATE request. */
 struct request_data *zone_update_dequeue(zone_t *zone);
 
+/*! \brief Returns true if final SOA in transfer has newer serial than zone */
+bool zone_transfer_needed(const zone_t *zone, const knot_pkt_t *pkt);
+
+
 /*! @} */
diff --git a/tests-extra/runtests.py b/tests-extra/runtests.py
index 31449bbb41210c0731d0e1e3be84f05f4d1fb352..b25c0c6bf0c437475d8d0d2985ceda102ab7fae4 100755
--- a/tests-extra/runtests.py
+++ b/tests-extra/runtests.py
@@ -69,6 +69,16 @@ def parse_args(cmd_args):
 
     return included, excluded
 
+def log_failed(log_dir, msg, indent=True):
+    fname = log_dir + "/failed.log"
+    first = False if os.path.isfile(fname) else True
+
+    file = open(fname, mode="a")
+    if first:
+        print("Failed tests:", file=file)
+    print("%s%s" % (" * " if indent else "", msg), file=file)
+    file.close()
+
 def main(args):
     included, excluded = parse_args(args)
 
@@ -128,6 +138,7 @@ def main(args):
             test_file = case_dir + "/test.py"
             if not os.path.isfile(test_file):
                 log.error(" * case \'%s\':\tMISSING" % case)
+                log_failed(outs_dir, "%s/%s\tMISSING" % (test, case))
                 fail_cnt += 1
                 continue
 
@@ -144,6 +155,8 @@ def main(args):
             except OsError:
                 log.error(" * case \'%s\':\tEXCEPTION (no dir \'%s\')" %
                           (case, out_dir))
+                log_failed(outs_dir, "%s/%s\tEXCEPTION (no dir \'%s\')" %
+                           (test, case, out_dir))
                 fail_cnt += 1
                 continue
 
@@ -159,6 +172,8 @@ def main(args):
                 else:
                     log.error(" * case \'%s\':\tEXCEPTION (%s)" %
                               (case, format(exc)))
+                log_failed(outs_dir, "%s/%s\tEXCEPTION (%s)" %
+                           (test, case, format(exc)))
                 fail_cnt += 1
             except BaseException as exc:
                 save_traceback(params.out_dir)
@@ -174,6 +189,7 @@ def main(args):
                 if params.err:
                     msg = " (%s)" % params.err_msg if params.err_msg else ""
                     log.info(" * case \'%s\':\tFAILED%s" % (case, msg))
+                    log_failed(outs_dir, "%s/%s\tFAILED%s" % (test, case, msg))
                     fail_cnt += 1
                 else:
                     log.info(" * case \'%s\':\tOK" % case)
@@ -190,6 +206,7 @@ def main(args):
     log.info(msg_cases + msg_skips + msg_res)
 
     if fail_cnt:
+        log_failed(outs_dir, "Total %i/%i" % (fail_cnt, case_cnt), indent=False)
         sys.exit(1)
     else:
         sys.exit(0)
diff --git a/tests/journal.c b/tests/journal.c
index 6a11d3f3e925d61c8cc22ce728ab7c5a2e5ad69b..43611e9f10a3352b3c38b2964068e6c129eeee84 100644
--- a/tests/journal.c
+++ b/tests/journal.c
@@ -38,7 +38,7 @@ static int randstr(char* dst, size_t len)
 
 int main(int argc, char *argv[])
 {
-	plan(7);
+	plan(10);
 
 	/* Create tmpdir */
 	int fsize = 10 * 1024 * 1024;
@@ -68,24 +68,33 @@ int main(int argc, char *argv[])
 	char chk_buf[64] = {'\0'};
 	randstr(chk_buf, sizeof(chk_buf));
 	int ret = journal_map(journal, chk_key, &mptr, sizeof(chk_buf), false);
-	memcpy(mptr, chk_buf, sizeof(chk_buf));
-	journal_unmap(journal, chk_key, mptr, 1);
-	is_int(0, ret, "journal: write data");
+	is_int(KNOT_EOK, ret, "journal: write data (map)");
+	if (ret == KNOT_EOK) {
+		memcpy(mptr, chk_buf, sizeof(chk_buf));
+		ret = journal_unmap(journal, chk_key, mptr, 1);
+		is_int(KNOT_EOK, ret, "journal: write data (unmap)");
+	}
 
-	journal_map(journal, chk_key, &mptr, sizeof(chk_buf), true);
-	ret = memcmp(chk_buf, mptr, sizeof(chk_buf));
-	journal_unmap(journal, chk_key, mptr, 1);
-	is_int(0, ret, "journal: data integrity check");
+	ret = journal_map(journal, chk_key, &mptr, sizeof(chk_buf), true);
+	is_int(KNOT_EOK, ret, "journal: data integrity check (map)");
+	if (ret == KNOT_EOK) {
+		ret = memcmp(chk_buf, mptr, sizeof(chk_buf));
+		is_int(0, ret, "journal: data integrity check (cmp)");
+		ret = journal_unmap(journal, chk_key, mptr, 0);
+		is_int(KNOT_EOK, ret, "journal: data integrity check (unmap)");
+	}
 
 	/* Reopen log and re-read value. */
 	journal_close(journal);
 	journal = journal_open(jfilename, fsize);
 	ok(journal != NULL, "journal: open journal '%s'", jfilename);
 
-	journal_map(journal, chk_key, &mptr, sizeof(chk_buf), true);
-	ret = memcmp(chk_buf, mptr, sizeof(chk_buf));
-	journal_unmap(journal, chk_key, mptr, 1);
-	is_int(0, ret, "journal: data integrity check after close/open");
+	ret = journal_map(journal, chk_key, &mptr, sizeof(chk_buf), true);
+	if (ret == KNOT_EOK) {
+		ret = memcmp(chk_buf, mptr, sizeof(chk_buf));
+		journal_unmap(journal, chk_key, mptr, 0);
+	}
+	is_int(KNOT_EOK, ret, "journal: data integrity check after close/open");
 
 	/*  Write random data. */
 	ret = 0;
@@ -103,7 +112,7 @@ int main(int argc, char *argv[])
 			break;
 		}
 	}
-	is_int(0, ret, "journal: sustained mmap r/w");
+	is_int(KNOT_EOK, ret, "journal: sustained mmap r/w");
 
 	/* Close journal. */
 	journal_close(journal);
diff --git a/tests/pkt.c b/tests/pkt.c
index d0d5d58f3ba4d20504f89ee9743ebd4aef70abc3..1d3efca88003cfee583533fc17a07805a3bd96ca 100644
--- a/tests/pkt.c
+++ b/tests/pkt.c
@@ -66,7 +66,7 @@ static void packet_match(knot_pkt_t *in, knot_pkt_t *out)
 
 int main(int argc, char *argv[])
 {
-	plan(30);
+	plan(29);
 
 	/* Create memory pool context. */
 	int ret = 0;
diff --git a/tests/requestor.c b/tests/requestor.c
index a8b32236f47f41db66f2163694bc2a0ad7bcabbe..cbe2eee9266c9baa298ba4b6f78f8a3697feaf83 100644
--- a/tests/requestor.c
+++ b/tests/requestor.c
@@ -64,28 +64,28 @@ static void* responder_thread(void *arg)
 #define CONNECTED_TESTS    4
 #define TESTS_COUNT DISCONNECTED_TESTS + CONNECTED_TESTS
 
-static struct request *make_query(struct requestor *requestor, struct sockaddr_storage *remote)
+static struct request *make_query(struct requestor *requestor,  conf_iface_t *remote)
 {
 	knot_pkt_t *pkt = knot_pkt_new(NULL, KNOT_WIRE_MAX_PKTSIZE, requestor->mm);
 	assert(pkt);
 	knot_pkt_put_question(pkt, ROOT_DNAME, KNOT_CLASS_IN, KNOT_RRTYPE_SOA);
 
-	return requestor_make(requestor, NULL, remote, pkt);
+	return requestor_make(requestor, remote, pkt);
 }
 
-static void test_disconnected(struct requestor *requestor, struct sockaddr_storage *remote)
+static void test_disconnected(struct requestor *requestor, conf_iface_t *remote)
 {
 	/* Enqueue packet. */
 	int ret = requestor_enqueue(requestor, make_query(requestor, remote), NULL);
-	is_int(KNOT_EOK, ret, "requestor: disconnected/enqueue");
+	is_int(KNOT_ECONN, ret, "requestor: disconnected/enqueue");
 
 	/* Wait for completion. */
 	struct timeval tv = { 5, 0 };
 	ret = requestor_exec(requestor, &tv);
-	is_int(KNOT_ECONNREFUSED, ret, "requestor: disconnected/wait");
+	is_int(KNOT_ENOENT, ret, "requestor: disconnected/wait");
 }
 
-static void test_connected(struct requestor *requestor, struct sockaddr_storage *remote)
+static void test_connected(struct requestor *requestor, conf_iface_t *remote)
 {
 	/* Enqueue packet. */
 	int ret = requestor_enqueue(requestor, make_query(requestor, remote), NULL);;
@@ -118,9 +118,10 @@ int main(int argc, char *argv[])
 
 	mm_ctx_t mm;
 	mm_ctx_mempool(&mm, 4096);
-	struct sockaddr_storage remote, origin;
-	sockaddr_set(&remote, AF_INET, "127.0.0.1", 0);
-	sockaddr_set(&origin, AF_INET, "127.0.0.1", 0);
+	conf_iface_t remote;
+	memset(&remote, 0, sizeof(conf_iface_t));
+	sockaddr_set(&remote.addr, AF_INET, "127.0.0.1", 0);
+	sockaddr_set(&remote.via, AF_INET, "127.0.0.1", 0);
 
 	/* Create fake server environment. */
 	server_t server;
@@ -134,10 +135,10 @@ int main(int argc, char *argv[])
 	test_disconnected(&requestor, &remote);
 
 	/* Bind to random port. */
-	int origin_fd = net_bound_socket(SOCK_STREAM, &remote);
+	int origin_fd = net_bound_socket(SOCK_STREAM, &remote.addr);
 	assert(origin_fd > 0);
-	socklen_t addr_len = sockaddr_len(&remote);
-	getsockname(origin_fd, (struct sockaddr *)&remote, &addr_len);
+	socklen_t addr_len = sockaddr_len(&remote.addr);
+	getsockname(origin_fd, (struct sockaddr *)&remote.addr, &addr_len);
 	int ret = listen(origin_fd, 10);
 	assert(ret == 0);
 
@@ -152,7 +153,7 @@ int main(int argc, char *argv[])
 #warning TODO: when ported sign_packet/verify_packet
 
 	/* Terminate responder. */
-	int responder = net_connected_socket(SOCK_STREAM, &remote, NULL, 0);
+	int responder = net_connected_socket(SOCK_STREAM, &remote.addr, NULL, 0);
 	assert(responder > 0);
 	tcp_send(responder, (const uint8_t *)"", 1);
 	(void) pthread_join(thread, 0);