diff --git a/src/knot/updates/ddns.c b/src/knot/updates/ddns.c
index 0a7afc2bad03c6e2970349f5e6de7e1873c0e844..5aa304059e039c66522015b7537559dd8b045bd0 100644
--- a/src/knot/updates/ddns.c
+++ b/src/knot/updates/ddns.c
@@ -31,99 +31,9 @@
 #include "common/descriptor.h"
 #include "common/lists.h"
 
-static bool list_contains_rr(const list_t *l, const knot_rrset_t *rr)
-{
-	knot_rr_ln_t *n;
-	WALK_LIST(n, *l) {
-		const knot_rrset_t *list_rr = n->rr;
-		if (knot_rrset_equal(rr, list_rr, KNOT_RRSET_COMPARE_WHOLE)) {
-			return true;
-		}
-	};
-
-	return false;
-}
-
-static bool removed_rr(const knot_changeset_t *changeset, const knot_rrset_t *rr)
-{
-	return list_contains_rr(&changeset->remove, rr);
-}
-
-static void remove_rr_from_list(list_t *l, const knot_rrset_t *rr)
-{
-	knot_rr_ln_t *rr_node = NULL;
-	node_t *nxt = NULL;
-	WALK_LIST_DELSAFE(rr_node, nxt, *l) {
-		knot_rrset_t *rrset = rr_node->rr;
-		if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_WHOLE)) {
-			knot_rrset_free(&rrset, NULL);
-			rem_node((node_t *)rr_node);
-			return;
-		}
-	}
-}
-
-static void remove_header_from_list(list_t *l, const knot_rrset_t *rr)
-{
-	knot_rr_ln_t *rr_node = NULL;
-	node_t *nxt = NULL;
-	WALK_LIST_DELSAFE(rr_node, nxt, *l) {
-		knot_rrset_t *rrset = rr_node->rr;
-		if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_HEADER)) {
-			knot_rrset_free(&rrset, NULL);
-			rem_node((node_t *)rr_node);
-		}
-	}
-}
-
-static void remove_owner_from_list(list_t *l, const knot_dname_t *owner)
-{
-	knot_rr_ln_t *rr_node = NULL;
-	node_t *nxt = NULL;
-	WALK_LIST_DELSAFE(rr_node, nxt, *l) {
-		knot_rrset_t *rrset = rr_node->rr;
-		if (knot_dname_is_equal(rrset->owner, owner)) {
-			knot_rrset_free(&rrset, NULL);
-			rem_node((node_t *)rr_node);
-		}
-	}
-}
-
-static bool node_empty(const knot_node_t *node, const knot_changeset_t *changeset)
-{
-	if (node == NULL) {
-		return true;
-	}
-
-	for (uint16_t i = 0; i < node->rrset_count; ++i) {
-		knot_rrset_t node_rrset = NODE_RR_INIT_N(node, i);
-		knot_rrset_t node_rr;
-		knot_rrset_init(&node_rr, node->owner, node_rrset.type, KNOT_CLASS_IN);
-		for (uint16_t j = 0; j < node_rrset.rrs.rr_count; ++j) {
-			knot_rrset_add_rr_from_rrset(&node_rr, &node_rrset, j, NULL);
-			if (!removed_rr(changeset, &node_rr)) {
-				knot_rrs_clear(&node_rr.rrs, NULL);
-				return false;
-			}
-			knot_rrs_clear(&node_rr.rrs, NULL);
-		}
-	}
-
-	return true;
-}
-
-static bool rrset_empty(const knot_rrset_t *rrset)
-{
-	uint16_t rr_count = knot_rrset_rr_count(rrset);
-	if (rr_count == 0) {
-		return true;
-	}
-	if (rr_count == 1) {
-		return knot_rrset_rr_size(rrset, 0) == 0;
-	}
-	return false;
-}
+/* ----------------------------- prereq check ------------------------------- */
 
+/*!< \brief Clears prereq RRSet list. */
 static void rrset_list_clear(list_t *l)
 {
 	node_t *n, *nxt;
@@ -135,6 +45,7 @@ static void rrset_list_clear(list_t *l)
 	};
 }
 
+/*!< \brief Adds RR to prereq RRSet list, merges RRs into RRSets. */
 static int add_rr_to_list(list_t *l, const knot_rrset_t *rr)
 {
 	node_t *n;
@@ -157,6 +68,7 @@ static int add_rr_to_list(list_t *l, const knot_rrset_t *rr)
 	return ptrlist_add(l, rr_copy, NULL) != NULL ? KNOT_EOK : KNOT_ENOMEM;
 }
 
+/*!< \brief Checks whether RR type exists in the zone. */
 static int knot_ddns_check_exist(const knot_zone_contents_t *zone,
                                  const knot_rrset_t *rrset, uint16_t *rcode)
 {
@@ -185,6 +97,7 @@ static int knot_ddns_check_exist(const knot_zone_contents_t *zone,
 	return KNOT_EOK;
 }
 
+/*!< \brief Checks whether RRSet exists in the zone. */
 static int knot_ddns_check_exist_full(const knot_zone_contents_t *zone,
                                       const knot_rrset_t *rrset,
                                       uint16_t *rcode)
@@ -210,10 +123,7 @@ static int knot_ddns_check_exist_full(const knot_zone_contents_t *zone,
 		return KNOT_EPREREQ;
 	} else {
 		knot_rrset_t found = NODE_RR_INIT(node, rrset->type);
-		// do not have to compare the header, it is already done
-		assert((&found)->type == rrset->type);
-		assert(knot_dname_cmp((&found)->owner,
-		                          rrset->owner) == 0);
+		assert(!knot_rrset_empty(&found));
 		if (!knot_rrset_equal(&found, rrset, KNOT_RRSET_COMPARE_WHOLE)) {
 			*rcode = KNOT_RCODE_NXRRSET;
 			return KNOT_EPREREQ;
@@ -223,6 +133,7 @@ static int knot_ddns_check_exist_full(const knot_zone_contents_t *zone,
 	return KNOT_EOK;
 }
 
+/*!< \brief Checks whether RRSets in the list exist in the zone. */
 static int check_exists_in_list(list_t *l, const knot_zone_contents_t *zone,
                                 uint16_t *rcode)
 {
@@ -239,6 +150,7 @@ static int check_exists_in_list(list_t *l, const knot_zone_contents_t *zone,
 	return KNOT_EOK;
 }
 
+/*!< \brief Checks whether RR type is not in the zone. */
 static int knot_ddns_check_not_exist(const knot_zone_contents_t *zone,
                                      const knot_rrset_t *rrset,
                                      uint16_t *rcode)
@@ -264,12 +176,11 @@ static int knot_ddns_check_not_exist(const knot_zone_contents_t *zone,
 		return KNOT_EOK;
 	}
 
-	/* RDATA is always empty for simple RRset checks. */
-
 	*rcode = KNOT_RCODE_YXRRSET;
 	return KNOT_EPREREQ;
 }
 
+/*!< \brief Checks whether DNAME is in the zone. */
 static int knot_ddns_check_in_use(const knot_zone_contents_t *zone,
                                   const knot_dname_t *dname,
                                   uint16_t *rcode)
@@ -298,6 +209,7 @@ static int knot_ddns_check_in_use(const knot_zone_contents_t *zone,
 	return KNOT_EOK;
 }
 
+/*!< \brief Checks whether DNAME is not in the zone. */
 static int knot_ddns_check_not_in_use(const knot_zone_contents_t *zone,
                                       const knot_dname_t *dname,
                                       uint16_t *rcode)
@@ -325,6 +237,20 @@ static int knot_ddns_check_not_in_use(const knot_zone_contents_t *zone,
 	return KNOT_EPREREQ;
 }
 
+/*!< \brief Returns true if rrset has 0 data or RDATA of size 0 (we need TTL). */
+static bool rrset_empty(const knot_rrset_t *rrset)
+{
+	uint16_t rr_count = knot_rrset_rr_count(rrset);
+	if (rr_count == 0) {
+		return true;
+	}
+	if (rr_count == 1) {
+		return knot_rrset_rr_size(rrset, 0) == 0;
+	}
+	return false;
+}
+
+/*!< \brief Checks prereq for given packet RR. */
 static int knot_ddns_check_prereq(const knot_rrset_t *rrset,
                                   uint16_t qclass,
                                   const knot_zone_contents_t *zone,
@@ -357,6 +283,7 @@ static int knot_ddns_check_prereq(const knot_rrset_t *rrset,
 			return knot_ddns_check_not_exist(zone, rrset, rcode);
 		}
 	} else if (rrset->rclass == qclass) {
+		// Store RRs for full check into list
 		return add_rr_to_list(rrset_list, rrset);
 	} else {
 		dbg_ddns("ddns: add_prereq: Bad class.\n");
@@ -364,111 +291,68 @@ static int knot_ddns_check_prereq(const knot_rrset_t *rrset,
 	}
 }
 
-/* API functions                                                              */
+/* --------------------------- DDNS processing ------------------------------ */
 
+/* ----------------------- changeset lists helpers -------------------------- */
 
-int knot_ddns_check_zone(const knot_zone_contents_t *zone,
-                         const knot_pkt_t *query, uint16_t *rcode)
+/*!< \brief Checks whether RR was already removed. */
+static bool removed_rr(const knot_changeset_t *changeset, const knot_rrset_t *rr)
 {
-	if (zone == NULL || query == NULL || rcode == NULL) {
-		if (rcode != NULL) {
-			*rcode = KNOT_RCODE_SERVFAIL;
+	knot_rr_ln_t *n;
+	WALK_LIST(n, changeset->remove) {
+		const knot_rrset_t *list_rr = n->rr;
+		if (knot_rrset_equal(rr, list_rr, KNOT_RRSET_COMPARE_WHOLE)) {
+			return true;
 		}
-		return KNOT_EINVAL;
-	}
-
-	if (knot_pkt_qtype(query) != KNOT_RRTYPE_SOA) {
-		*rcode = KNOT_RCODE_FORMERR;
-		return KNOT_EMALF;
-	}
-
-	// check zone CLASS
-	if (knot_pkt_qclass(query) != KNOT_CLASS_IN) {
-		*rcode = KNOT_RCODE_NOTAUTH;
-		return KNOT_ENOZONE;
-	}
+	};
 
-	return KNOT_EOK;
+	return false;
 }
 
-int knot_ddns_process_prereqs(const knot_pkt_t *query, const knot_zone_contents_t *zone,
-                              uint16_t *rcode)
+/*!< \brief Removes RR from list, full equality check. */
+static void remove_rr_from_list(list_t *l, const knot_rrset_t *rr)
 {
-	if (query == NULL || rcode == NULL || zone == NULL) {
-		return KNOT_EINVAL;
-	}
-
-	dbg_ddns("Processing prerequisities.\n");
-
-	int ret = KNOT_EOK;
-	list_t rrset_list; // List used to store merged RRSets
-	init_list(&rrset_list);
-
-	const knot_pktsection_t *answer = knot_pkt_section(query, KNOT_ANSWER);
-	for (int i = 0; i < answer->count; ++i) {
-		// Check what can be checked, store full RRs into list
-		ret = knot_ddns_check_prereq(&answer->rr[i],
-		                             knot_pkt_qclass(query),
-		                             zone, rcode, &rrset_list);
-		if (ret != KNOT_EOK) {
-			rrset_list_clear(&rrset_list);
-			return ret;
+	knot_rr_ln_t *rr_node = NULL;
+	node_t *nxt = NULL;
+	WALK_LIST_DELSAFE(rr_node, nxt, *l) {
+		knot_rrset_t *rrset = rr_node->rr;
+		if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_WHOLE)) {
+			knot_rrset_free(&rrset, NULL);
+			rem_node((node_t *)rr_node);
+			return;
 		}
 	}
-
-	// Check stored RRSets
-	ret = check_exists_in_list(&rrset_list, zone, rcode);
-	rrset_list_clear(&rrset_list);
-	return ret;
 }
 
-static int knot_ddns_check_update(const knot_rrset_t *rrset,
-                                  const knot_pkt_t *query,
-                                  uint16_t *rcode)
+/*!< \brief Removes RR from list, owner and type check. */
+static void remove_header_from_list(list_t *l, const knot_rrset_t *rr)
 {
-	/* Accept both subdomain and dname match. */
-	dbg_ddns("Checking UPDATE packet.\n");
-	const knot_dname_t *owner = rrset->owner;
-	const knot_dname_t *qname = knot_pkt_qname(query);
-	int is_sub = knot_dname_is_sub(owner, qname);
-	if (!is_sub && knot_dname_cmp(owner, qname) != 0) {
-		*rcode = KNOT_RCODE_NOTZONE;
-		return KNOT_EOUTOFZONE;
-	}
-
-	if (knot_rrtype_is_ddns_forbidden(rrset->type)) {
-		*rcode = KNOT_RCODE_REFUSED;
-		log_zone_error("Refusing to update DNSSEC-related record!\n");
-		return KNOT_EDENIED;
+	knot_rr_ln_t *rr_node = NULL;
+	node_t *nxt = NULL;
+	WALK_LIST_DELSAFE(rr_node, nxt, *l) {
+		knot_rrset_t *rrset = rr_node->rr;
+		if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_HEADER)) {
+			knot_rrset_free(&rrset, NULL);
+			rem_node((node_t *)rr_node);
+		}
 	}
+}
 
-	if (rrset->rclass == knot_pkt_qclass(query)) {
-		if (knot_rrtype_is_metatype(rrset->type)) {
-			*rcode = KNOT_RCODE_FORMERR;
-			return KNOT_EMALF;
-		}
-	} else if (rrset->rclass == KNOT_CLASS_ANY) {
-		if (!rrset_empty(rrset)
-		    || (knot_rrtype_is_metatype(rrset->type)
-		        && rrset->type != KNOT_RRTYPE_ANY)) {
-			*rcode = KNOT_RCODE_FORMERR;
-			return KNOT_EMALF;
-		}
-	} else if (rrset->rclass == KNOT_CLASS_NONE) {
-		if (knot_rrset_rr_ttl(rrset, 0) != 0
-		    || knot_rrtype_is_metatype(rrset->type)) {
-			*rcode = KNOT_RCODE_FORMERR;
-			return KNOT_EMALF;
+/*!< \brief Removes RR from list, owner check. */
+static void remove_owner_from_list(list_t *l, const knot_dname_t *owner)
+{
+	knot_rr_ln_t *rr_node = NULL;
+	node_t *nxt = NULL;
+	WALK_LIST_DELSAFE(rr_node, nxt, *l) {
+		knot_rrset_t *rrset = rr_node->rr;
+		if (knot_dname_is_equal(rrset->owner, owner)) {
+			knot_rrset_free(&rrset, NULL);
+			rem_node((node_t *)rr_node);
 		}
-	} else {
-		*rcode = KNOT_RCODE_FORMERR;
-		return KNOT_EMALF;
 	}
-
-	return KNOT_EOK;
 }
 
-/* DDNS processing */
+/* --------------------- true/false helper functions ------------------------ */
 
 static inline bool is_addition(const knot_rrset_t *rr)
 {
@@ -495,6 +379,7 @@ static inline bool is_node_removal(const knot_rrset_t *rr)
 	return rr->rclass == KNOT_CLASS_ANY && rr->type == KNOT_RRTYPE_ANY;
 }
 
+/*!< \brief Returns true if last addition of certain types is to be replaced. */
 static bool should_replace(const knot_rrset_t *chg_rrset,
                            const knot_rrset_t *rrset)
 {
@@ -507,6 +392,86 @@ 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 knot_node_t *node, const knot_changeset_t *changeset)
+{
+	if (node == NULL) {
+		return true;
+	}
+
+	for (uint16_t i = 0; i < node->rrset_count; ++i) {
+		knot_rrset_t node_rrset = NODE_RR_INIT_N(node, i);
+		knot_rrset_t node_rr;
+		knot_rrset_init(&node_rr, node->owner, node_rrset.type, KNOT_CLASS_IN);
+		for (uint16_t j = 0; j < node_rrset.rrs.rr_count; ++j) {
+			knot_rrset_add_rr_from_rrset(&node_rr, &node_rrset, j, NULL);
+			if (!removed_rr(changeset, &node_rr)) {
+				knot_rrs_clear(&node_rr.rrs, NULL);
+				return false;
+			}
+			knot_rrs_clear(&node_rr.rrs, NULL);
+		}
+	}
+
+	return true;
+}
+
+/*!< \brief Returns true if node contains given RR in its RRSets. */
+static bool node_contains_rr(const knot_node_t *node,
+                             const knot_rrset_t *rr)
+{
+	knot_rrset_t zone_rrset = NODE_RR_INIT(node, rr->type);
+	if (!knot_rrset_empty(&zone_rrset)) {
+		knot_rrset_t intersection;
+		int ret = knot_rrset_intersection(&zone_rrset, rr,
+		                                  &intersection, NULL);
+		if (ret != KNOT_EOK) {
+			return false;
+		}
+		const bool contains = !knot_rrset_empty(&intersection);
+		knot_rrs_clear(&intersection.rrs, NULL);
+		return contains;
+	} else {
+		return false;
+	}
+}
+
+/*!< \brief Returns true if CNAME is in this node. */
+static bool adding_to_cname(const knot_node_t *node,
+                            knot_changeset_t *changeset)
+{
+	if (node == NULL) {
+		// Node did not exist before update.
+		return false;
+	}
+
+	knot_rrset_t cname = NODE_RR_INIT(node, KNOT_RRTYPE_CNAME);
+	if (knot_rrset_empty(&cname)) {
+		// Node did not contain CNAME before update.
+		return false;
+	}
+
+	// Return true if we have not removed CNAME in this update.
+	return !removed_rr(changeset, &cname);
+}
+
+/*!< \brief Used to ignore SOA deletions and SOAs with lower serial than zone. */
+static bool skip_soa(const knot_rrset_t *rr, int64_t sn)
+{
+	if (rr->type == KNOT_RRTYPE_SOA
+	    && (rr->rclass == KNOT_CLASS_NONE
+	        || rr->rclass == KNOT_CLASS_ANY
+	        || knot_serial_compare(knot_rrs_soa_serial(&rr->rrs),
+	                               sn) <= 0)) {
+		return true;
+	}
+
+	return false;
+}
+
+/* ---------------------- changeset manipulation ---------------------------- */
+
+/*!< \brief Checks whether record should be added or replaced. */
 static bool skip_record_addition(knot_changeset_t *changeset,
                                  knot_rrset_t *rr)
 {
@@ -514,10 +479,12 @@ static bool skip_record_addition(knot_changeset_t *changeset,
 	WALK_LIST(rr_node, changeset->add) {
 		knot_rrset_t *rrset = rr_node->rr;
 		if (should_replace(rr, rrset)) {
+			// Replacing singleton RR.
 			knot_rrset_free(&rrset, NULL);
 			rrset = rr;
 			return true;
 		} else if (knot_rrset_equal(rr, rrset, KNOT_RRSET_COMPARE_WHOLE)) {
+			// Freeing duplication.
 			knot_rrset_free(&rr, NULL);
 			return true;
 		}
@@ -526,6 +493,7 @@ static bool skip_record_addition(knot_changeset_t *changeset,
 	return false;
 }
 
+/*!< \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,
                             int *apex_ns_rem)
 {
@@ -541,12 +509,14 @@ static int add_rr_to_chgset(const knot_rrset_t *rr, knot_changeset_t *changeset,
 	}
 
 	if (apex_ns_rem) {
+		// Increase post update apex NS count.
 		(*apex_ns_rem)--;
 	}
 
 	return knot_changeset_add_rrset(changeset, rr_copy, KNOT_CHANGESET_ADD);
 }
 
+/*!< \brief Checks whether record should be removed (duplicate check). */
 static bool skip_record_removal(knot_changeset_t *changeset, knot_rrset_t *rr)
 {
 	knot_rr_ln_t *rr_node = NULL;
@@ -559,21 +529,10 @@ static bool skip_record_removal(knot_changeset_t *changeset, knot_rrset_t *rr)
 		}
 	}
 
-	node_t *nxt = NULL;
-	WALK_LIST_DELSAFE(rr_node, nxt, changeset->add) {
-		knot_rrset_t *rrset = rr_node->rr;
-		if (knot_rrset_equal(rrset, rr, KNOT_RRSET_COMPARE_WHOLE)) {
-			// Adding and removing identical RRs, drop both.
-			knot_rrset_free(&rrset, NULL);
-			knot_rrset_free(&rr, NULL);
-			rem_node((node_t *)rr_node);
-			return true;
-		}
-	}
-
 	return false;
 }
 
+/*!< \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,
                             int *apex_ns_rem)
 {
@@ -589,12 +548,14 @@ static int rem_rr_to_chgset(const knot_rrset_t *rr, knot_changeset_t *changeset,
 	}
 
 	if (apex_ns_rem) {
+		// Decrease post update apex NS count.
 		(*apex_ns_rem)++;
 	}
 
 	return knot_changeset_add_rrset(changeset, rr_copy, KNOT_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,
                                int *apex_ns_rem)
@@ -616,6 +577,11 @@ static int rem_rrset_to_chgset(const knot_rrset_t *rrset,
 	return KNOT_EOK;
 }
 
+/* ------------------------ RR processing logic ----------------------------- */
+
+/* --------------------------- RR additions --------------------------------- */
+
+/*!< \brief Processes CNAME addition (replace or ignore) */
 static int process_add_cname(const knot_node_t *node,
                              const knot_rrset_t *rr,
                              knot_changeset_t *changeset)
@@ -637,12 +603,14 @@ static int process_add_cname(const knot_node_t *node,
 		// Other occupied node => ignore.
 		return KNOT_EOK;
 	} else {
+		// Can add.
 		return add_rr_to_chgset(rr, changeset, NULL);
 	}
 
 	return KNOT_EOK;
 }
 
+/*!< \brief Processes CNAME addition (ignore when not removed, or non-apex) */
 static int process_add_nsec3param(const knot_node_t *node,
                                   const knot_rrset_t *rr,
                                   knot_changeset_t *changeset)
@@ -669,14 +637,81 @@ static int process_add_nsec3param(const knot_node_t *node,
 	return KNOT_EOK;
 }
 
+/*!
+ * \brief Processes SOA addition (ignore when non-apex), lower serials
+ *        dropped before.
+ */
+static int process_add_soa(const knot_node_t *node,
+                           const knot_rrset_t *rr,
+                           knot_changeset_t *changeset)
+{
+	if (node == NULL || !knot_node_rrtype_exists(node, KNOT_RRTYPE_SOA)) {
+		// Adding SOA to non-apex node, ignore
+		return KNOT_EOK;
+	}
+
+	/* Get the current SOA RR from the node. */
+	knot_rrset_t removed = NODE_RR_INIT(node, KNOT_RRTYPE_SOA);
+	/* If they are identical, ignore. */
+	if (knot_rrset_equal(&removed, rr, KNOT_RRSET_COMPARE_WHOLE)) {
+		return KNOT_EOK;
+	}
+	return add_rr_to_chgset(rr, changeset, NULL);
+}
+
+/*!< \brief Adds normal RR, ignores when CNAME exists in node. */
+static int process_add_normal(const knot_node_t *node,
+                              const knot_rrset_t *rr,
+                              knot_changeset_t *changeset,
+                              int *apex_ns_rem)
+{
+	if (adding_to_cname(node, changeset)) {
+		// Adding RR to CNAME node, ignore.
+		return KNOT_EOK;
+	}
+
+	if (node && node_contains_rr(node, rr)) {
+		// Adding existing RR, remove removal from changeset if it's there.
+		remove_rr_from_list(&changeset->remove, rr);
+		// And ignore.
+		return KNOT_EOK;
+	}
+
+	const bool apex_ns = knot_node_rrtype_exists(node, KNOT_RRTYPE_SOA) &&
+	                     rr->type == KNOT_RRTYPE_NS;
+	return add_rr_to_chgset(rr, changeset, apex_ns ? apex_ns_rem : NULL);
+}
+
+/*!< \brief Decides what to do with RR addition. */
+static int process_add(const knot_rrset_t *rr,
+                       const knot_node_t *node,
+                       knot_changeset_t *changeset,
+                       int *apex_ns_rem)
+{
+	switch(rr->type) {
+	case KNOT_RRTYPE_CNAME:
+		return process_add_cname(node, rr, changeset);
+	case KNOT_RRTYPE_SOA:
+		return process_add_soa(node, rr, changeset);
+	case KNOT_RRTYPE_NSEC3PARAM:
+		return process_add_nsec3param(node, rr, changeset);
+	default:
+		return process_add_normal(node, rr, changeset, apex_ns_rem);
+	}
+}
+
+/* --------------------------- RR deletions --------------------------------- */
+
+/*!< \brief Removes single RR from zone. */
 static int process_rem_rr(const knot_rrset_t *rr,
                           const knot_node_t *node,
                           knot_changeset_t *changeset,
                           int *apex_ns_rem)
 {
+	// Remove possible previously added RR
+	remove_rr_from_list(&changeset->add, rr);
 	if (node == NULL) {
 		// Removing from node that did not exists before update
-		remove_rr_from_list(&changeset->add, rr);
 		return KNOT_EOK;
 	}
 
@@ -693,8 +728,7 @@ static int process_rem_rr(const knot_rrset_t *rr,
 
 	knot_rrset_t to_modify = NODE_RR_INIT(node, rr->type);
 	if (knot_rrset_empty(&to_modify)) {
-		// No such RRSet, but check duplicates
-		remove_rr_from_list(&changeset->add, rr);
+		// No such RRSet
 		return KNOT_EOK;
 	}
 
@@ -705,8 +739,7 @@ static int process_rem_rr(const knot_rrset_t *rr,
 	}
 
 	if (knot_rrset_empty(&intersection)) {
-		// No such RR, but check duplicates
-		remove_rr_from_list(&changeset->add, rr);
+		// No such RR
 		return KNOT_EOK;
 	}
 	assert(intersection.rrs.rr_count == 1);
@@ -716,47 +749,50 @@ static int process_rem_rr(const knot_rrset_t *rr,
 	return ret;
 }
 
+/*!< \brief Removes RRSet from zone. */
 static int process_rem_rrset(const knot_rrset_t *rrset,
                              const knot_node_t *node,
                              knot_changeset_t *changeset)
 {
-	// Removing all added RRs with this owner and type
+	// Removing all previously added RRs with this owner and type from changeset
 	remove_header_from_list(&changeset->add, rrset);
 	if (node == NULL) {
 		return KNOT_EOK;
 	}
-	uint16_t type = rrset->type;
 
-	if (type == KNOT_RRTYPE_SOA || knot_rrtype_is_ddns_forbidden(type)) {
+	if (rrset->type == KNOT_RRTYPE_SOA ||
+	    knot_rrtype_is_ddns_forbidden(rrset->type)) {
 		// Ignore SOA and DNSSEC removals.
 		return KNOT_EOK;
 	}
 
-	if (knot_node_rrtype_exists(node, KNOT_RRTYPE_SOA) && type == KNOT_RRTYPE_NS) {
-		// Ignore whole NS apex removals.
+	if (knot_node_rrtype_exists(node, KNOT_RRTYPE_SOA) &&
+	    rrset->type == KNOT_RRTYPE_NS) {
+		// Ignore NS apex RRSet removals.
 		return KNOT_EOK;
 	}
 
-	// no such RR
-	if (!knot_node_rrtype_exists(node, type)) {
-		// ignore
+	if (!knot_node_rrtype_exists(node, rrset->type)) {
+		// no such RR, ignore
 		return KNOT_EOK;
 	}
 
-	knot_rrset_t to_remove = NODE_RR_INIT(node, type);
+	knot_rrset_t to_remove = NODE_RR_INIT(node, rrset->type);
 	return rem_rrset_to_chgset(&to_remove, changeset, NULL);
 }
 
+/*!< \brief Removes node from zone. */
 static int process_rem_node(const knot_rrset_t *rr,
                             const knot_node_t *node, knot_changeset_t *changeset)
 {
-	// Remove all added records
+	// Remove all previously added records with given owner from changeset
 	remove_owner_from_list(&changeset->add, rr->owner);
 
 	if (node == NULL) {
 		return KNOT_EOK;
 	}
 
+	// Remove all RRSets from node
 	for (int i = 0; i < node->rrset_count; ++i) {
 		knot_rrset_t rrset = NODE_RR_INIT_N(node, i);
 		int ret = process_rem_rrset(&rrset, node, changeset);
@@ -768,99 +804,7 @@ static int process_rem_node(const knot_rrset_t *rr,
 	return KNOT_EOK;
 }
 
-static int process_add_soa(const knot_node_t *node,
-                           const knot_rrset_t *rr,
-                           knot_changeset_t *changeset)
-{
-	if (node == NULL || !knot_node_rrtype_exists(node, KNOT_RRTYPE_SOA)) {
-		// Adding SOA to non-apex node, ignore
-		return KNOT_EOK;
-	}
-
-	/* Get the current SOA RR from the node. */
-	knot_rrset_t removed = NODE_RR_INIT(node, KNOT_RRTYPE_SOA);
-	/* If they are identical, ignore. */
-	if (knot_rrset_equal(&removed, rr, KNOT_RRSET_COMPARE_WHOLE)) {
-		return KNOT_EOK;
-	}
-	return add_rr_to_chgset(rr, changeset, NULL);
-}
-
-static bool node_contains_rr(const knot_node_t *node,
-                             const knot_rrset_t *rr)
-{
-	knot_rrset_t zone_rrset = NODE_RR_INIT(node, rr->type);
-	if (!knot_rrset_empty(&zone_rrset)) {
-		knot_rrset_t intersection;
-		int ret = knot_rrset_intersection(&zone_rrset, rr,
-		                                  &intersection, NULL);
-		if (ret != KNOT_EOK) {
-			return false;
-		}
-		const bool contains = !knot_rrset_empty(&intersection);
-		knot_rrs_clear(&intersection.rrs, NULL);
-		return contains;
-	} else {
-		return false;
-	}
-}
-
-static bool adding_to_cname(const knot_node_t *node,
-                            knot_changeset_t *changeset)
-{
-	if (node == NULL) {
-		// Node did not exist before update.
-		return false;
-	}
-
-	knot_rrset_t cname = NODE_RR_INIT(node, KNOT_RRTYPE_CNAME);
-	if (knot_rrset_empty(&cname)) {
-		// Node did not contain CNAME before update.
-		return false;
-	}
-
-	return !removed_rr(changeset, &cname);
-}
-
-static int process_add_normal(const knot_node_t *node,
-                              const knot_rrset_t *rr,
-                              knot_changeset_t *changeset,
-                              int *apex_ns_rem)
-{
-	if (adding_to_cname(node, changeset)) {
-		// Adding RR to CNAME node, ignore.
-		return KNOT_EOK;
-	}
-
-	if (node && node_contains_rr(node, rr)) {
-		// Adding existing RR, remove from changeset if it's there.
-		remove_rr_from_list(&changeset->remove, rr);
-		// And ignore.
-		return KNOT_EOK;
-	}
-
-	const bool apex_ns = knot_node_rrtype_exists(node, KNOT_RRTYPE_SOA) &&
-	                     rr->type == KNOT_RRTYPE_NS;
-	return add_rr_to_chgset(rr, changeset, apex_ns ? apex_ns_rem : NULL);
-}
-
-static int process_add(const knot_rrset_t *rr,
-                       const knot_node_t *node,
-                       knot_changeset_t *changeset,
-                       int *apex_ns_rem)
-{
-	switch(rr->type) {
-	case KNOT_RRTYPE_CNAME:
-		return process_add_cname(node, rr, changeset);
-	case KNOT_RRTYPE_SOA:
-		return process_add_soa(node, rr, changeset);
-	case KNOT_RRTYPE_NSEC3PARAM:
-		return process_add_nsec3param(node, rr, changeset);
-	default:
-		return process_add_normal(node, rr, changeset, apex_ns_rem);
-	}
-}
-
+/*!< \brief Decides what to with removal. */
 static int process_remove(const knot_rrset_t *rr,
                           const knot_node_t *node,
                           knot_changeset_t *changeset,
@@ -877,14 +821,9 @@ static int process_remove(const knot_rrset_t *rr,
 	}
 }
 
-static int knot_ddns_final_soa_to_chgset(knot_rrset_t *soa,
-                                         knot_changeset_t *changeset)
-{
-	knot_changeset_add_soa(changeset, soa, KNOT_CHANGESET_ADD);
-
-	return KNOT_EOK;
-}
+/* --------------------------- validity checks ------------------------------ */
 
+/*!< \brief Checks whether addition has not violated DNAME rules. */
 static bool sem_check(const knot_rrset_t *rr,
                       const knot_node_t *zone_node,
                       const knot_zone_contents_t *zone)
@@ -915,8 +854,56 @@ static bool sem_check(const knot_rrset_t *rr,
 	return true;
 }
 
+/*!< \brief Checks whether we can accept this RR. */
+static int knot_ddns_check_update(const knot_rrset_t *rrset,
+                                  const knot_pkt_t *query,
+                                  uint16_t *rcode)
+{
+	/* Accept both subdomain and dname match. */
+	dbg_ddns("Checking UPDATE packet.\n");
+	const knot_dname_t *owner = rrset->owner;
+	const knot_dname_t *qname = knot_pkt_qname(query);
+	int is_sub = knot_dname_is_sub(owner, qname);
+	if (!is_sub && knot_dname_cmp(owner, qname) != 0) {
+		*rcode = KNOT_RCODE_NOTZONE;
+		return KNOT_EOUTOFZONE;
+	}
+
+	if (knot_rrtype_is_ddns_forbidden(rrset->type)) {
+		*rcode = KNOT_RCODE_REFUSED;
+		log_zone_error("Refusing to update DNSSEC-related record!\n");
+		return KNOT_EDENIED;
+	}
+
+	if (rrset->rclass == knot_pkt_qclass(query)) {
+		if (knot_rrtype_is_metatype(rrset->type)) {
+			*rcode = KNOT_RCODE_FORMERR;
+			return KNOT_EMALF;
+		}
+	} else if (rrset->rclass == KNOT_CLASS_ANY) {
+		if (!rrset_empty(rrset)
+		    || (knot_rrtype_is_metatype(rrset->type)
+		        && rrset->type != KNOT_RRTYPE_ANY)) {
+			*rcode = KNOT_RCODE_FORMERR;
+			return KNOT_EMALF;
+		}
+	} else if (rrset->rclass == KNOT_CLASS_NONE) {
+		if (knot_rrset_rr_ttl(rrset, 0) != 0
+		    || knot_rrtype_is_metatype(rrset->type)) {
+			*rcode = KNOT_RCODE_FORMERR;
+			return KNOT_EMALF;
+		}
+	} else {
+		*rcode = KNOT_RCODE_FORMERR;
+		return KNOT_EMALF;
+	}
+
+	return KNOT_EOK;
+}
+
+/*!< \brief Checks RR and decides what to do with it. */
 static int knot_ddns_process_rr(const knot_rrset_t *rr,
-                                knot_zone_contents_t *zone,
+                                const knot_zone_contents_t *zone,
                                 knot_changeset_t *changeset,
                                 int *apex_ns_rem)
 {
@@ -936,24 +923,7 @@ static int knot_ddns_process_rr(const knot_rrset_t *rr,
 	}
 }
 
-/*
- * Check if the record is SOA. If yes, check the SERIAL.
- * If this record should cause the SOA to be replaced in the
- * zone, use it as the ending SOA.
- */
-static bool skip_soa(const knot_rrset_t *rr, int64_t sn)
-{
-	if (rr->type == KNOT_RRTYPE_SOA
-	    && (rr->rclass == KNOT_CLASS_NONE
-	        || rr->rclass == KNOT_CLASS_ANY
-	        || knot_serial_compare(knot_rrs_soa_serial(&rr->rrs),
-	                               sn) <= 0)) {
-		return true;
-	}
-
-	return false;
-}
-
+/*!< \brief Maps Knot return code to RCODE. */
 static uint16_t ret_to_rcode(int ret)
 {
 	if (ret == KNOT_EMALF) {
@@ -965,7 +935,40 @@ static uint16_t ret_to_rcode(int ret)
 	}
 }
 
-int knot_ddns_process_update(knot_zone_contents_t *zone,
+/* ---------------------------------- API ----------------------------------- */
+
+int knot_ddns_process_prereqs(const knot_pkt_t *query, const knot_zone_contents_t *zone,
+                              uint16_t *rcode)
+{
+	if (query == NULL || rcode == NULL || zone == NULL) {
+		return KNOT_EINVAL;
+	}
+
+	dbg_ddns("Processing prerequisities.\n");
+
+	int ret = KNOT_EOK;
+	list_t rrset_list; // List used to store merged RRSets
+	init_list(&rrset_list);
+
+	const knot_pktsection_t *answer = knot_pkt_section(query, KNOT_ANSWER);
+	for (int i = 0; i < answer->count; ++i) {
+		// Check what can be checked, store full RRs into list
+		ret = knot_ddns_check_prereq(&answer->rr[i],
+		                             knot_pkt_qclass(query),
+		                             zone, rcode, &rrset_list);
+		if (ret != KNOT_EOK) {
+			rrset_list_clear(&rrset_list);
+			return ret;
+		}
+	}
+
+	// Check stored RRSets
+	ret = check_exists_in_list(&rrset_list, zone, rcode);
+	rrset_list_clear(&rrset_list);
+	return ret;
+}
+
+int knot_ddns_process_update(const knot_zone_contents_t *zone,
                              const knot_pkt_t *query,
                              knot_changeset_t *changeset,
                              uint16_t *rcode, uint32_t new_serial)
@@ -977,25 +980,14 @@ int knot_ddns_process_update(knot_zone_contents_t *zone,
 	/* Copy base SOA RR. */
 	knot_rrset_t *soa_begin = knot_node_create_rrset(zone->apex,
 	                                                 KNOT_RRTYPE_SOA);
-	knot_rrset_t *soa_end = NULL;
 	knot_changeset_add_soa(changeset, soa_begin, KNOT_CHANGESET_REMOVE);
 
-	/* Current SERIAL */
-	int64_t sn = knot_rrs_soa_serial(&soa_begin->rrs);
-	int64_t sn_new;
-
-	/* Set the new serial according to policy. */
-	if (sn > -1) {
-		sn_new = new_serial;
-		assert(sn_new != KNOT_EINVAL);
-	} else {
-		*rcode = KNOT_RCODE_SERVFAIL;
-		return KNOT_EINVAL;
-	}
+	int64_t sn_old = knot_zone_serial(zone);
 
 	/* Process all RRs the Authority (Update) section. */
 
 	dbg_ddns("Processing UPDATE section.\n");
+	knot_rrset_t *soa_end = NULL;
 	int apex_ns_rem = 0;
 	const knot_pktsection_t *authority = knot_pkt_section(query, KNOT_AUTHORITY);
 	for (uint16_t i = 0; i < authority->count; ++i) {
@@ -1007,7 +999,7 @@ int knot_ddns_process_update(knot_zone_contents_t *zone,
 			return ret;
 		}
 
-		if (skip_soa(rr, sn)) {
+		if (skip_soa(rr, sn_old)) {
 			continue;
 		}
 
@@ -1023,8 +1015,7 @@ int knot_ddns_process_update(knot_zone_contents_t *zone,
 				knot_rrset_free(&soa_end, NULL);
 			}
 			int64_t sn_rr = knot_rrs_soa_serial(&rr->rrs);
-			assert(knot_serial_compare(sn_rr, sn) > 0);
-			sn_new = sn_rr;
+			assert(knot_serial_compare(sn_rr, sn_old) > 0);
 			soa_end = knot_rrset_cpy(rr, NULL);
 			if (soa_end == NULL) {
 				return KNOT_ENOMEM;
@@ -1044,8 +1035,9 @@ int knot_ddns_process_update(knot_zone_contents_t *zone,
 			*rcode = KNOT_RCODE_SERVFAIL;
 			return KNOT_ENOMEM;
 		}
-		knot_rrs_soa_serial_set(&soa_end->rrs, sn_new);
+		knot_rrs_soa_serial_set(&soa_end->rrs, new_serial);
 	}
 
-	return knot_ddns_final_soa_to_chgset(soa_end, changeset);
+	knot_changeset_add_soa(changeset, soa_end, KNOT_CHANGESET_ADD);
+	return KNOT_EOK;
 }
diff --git a/src/knot/updates/ddns.h b/src/knot/updates/ddns.h
index 7cccd47c6f3495ce029179f373c25d85ca54345c..67652615e95ed3f33213d78cbc73878898457ffb 100644
--- a/src/knot/updates/ddns.h
+++ b/src/knot/updates/ddns.h
@@ -2,6 +2,7 @@
  * \file ddns.h
  *
  * \author Lubos Slovak <lubos.slovak@nic.cz>
+ * \author Jan Kadlec <jan.kadlec@nic.cz>
  *
  * \brief Dynamic updates processing.
  *
@@ -30,17 +31,34 @@
 #include "knot/updates/changesets.h"
 #include "knot/zone/zone.h"
 #include "libknot/packet/pkt.h"
-#include "libknot/rrset.h"
 #include "libknot/dname.h"
-#include "libknot/consts.h"
-#include "common/lists.h"
 
-int knot_ddns_check_zone(const knot_zone_contents_t *zone,
-                         const knot_pkt_t *query, uint16_t *rcode);
-
-int knot_ddns_process_prereqs(const knot_pkt_t *query, const knot_zone_contents_t *zone, uint16_t *rcode);
+/*!
+ * \brief Checks update prerequisite section.
+ *
+ * \param query  DNS message containing the update.
+ * \param zone   Zone to be checked.
+ * \param rcode  Returned DNS RCODE.
+ *
+ * \return KNOT_E*
+ */
+int knot_ddns_process_prereqs(const knot_pkt_t *query,
+                              const knot_zone_contents_t *zone,
+                              uint16_t *rcode);
 
-int knot_ddns_process_update(knot_zone_contents_t *zone,
+/*!
+ * \brief Processes DNS update and creates a changeset out of it. Zone is left
+ *        intact.
+ *
+ * \param zone        Zone to be updated.
+ * \param query       DNS message containing the update.
+ * \param changeset   Output changeset.
+ * \param rcode       Output DNS RCODE.
+ * \param new_serial  New serial to use for updated zone.
+ *
+ * \return KNOT_E*
+ */
+int knot_ddns_process_update(const knot_zone_contents_t *zone,
                               const knot_pkt_t *query,
                               knot_changeset_t *changeset,
                               uint16_t *rcode, uint32_t new_serial);