diff --git a/src/main/java/com/sparrowwallet/drongo/dns/AuthenticatingResolver.java b/src/main/java/com/sparrowwallet/drongo/dns/AuthenticatingResolver.java new file mode 100644 index 0000000..ad9c2a8 --- /dev/null +++ b/src/main/java/com/sparrowwallet/drongo/dns/AuthenticatingResolver.java @@ -0,0 +1,59 @@ +package com.sparrowwallet.drongo.dns; + +import org.xbill.DNS.*; + +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Executor; + +public class AuthenticatingResolver implements Resolver { + private final Resolver delegate; + private boolean authenticated; + + public AuthenticatingResolver(Resolver delegate) { + this.delegate = delegate; + } + + @Override + public void setPort(int port) { + delegate.setPort(port); + } + + @Override + public void setTCP(boolean flag) { + delegate.setTCP(flag); + } + + @Override + public void setIgnoreTruncation(boolean flag) { + delegate.setIgnoreTruncation(flag); + } + + @Override + public void setEDNS(int version, int payloadSize, int flags, List options) { + delegate.setEDNS(version, payloadSize, flags, options); + } + + @Override + public void setTSIGKey(TSIG key) { + delegate.setTSIGKey(key); + } + + @Override + public void setTimeout(Duration timeout) { + delegate.setTimeout(timeout); + } + + @Override + public CompletionStage sendAsync(Message query, Executor executor) { + return delegate.sendAsync(query, executor).thenApply(response -> { + this.authenticated = response.getHeader().getFlag(Flags.AD); + return response; + }); + } + + public boolean isAuthenticated() { + return authenticated; + } +} diff --git a/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentResolver.java b/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentResolver.java index f92793a..ba642e2 100644 --- a/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentResolver.java +++ b/src/main/java/com/sparrowwallet/drongo/dns/DnsPaymentResolver.java @@ -1,5 +1,6 @@ package com.sparrowwallet.drongo.dns; +import com.sparrowwallet.drongo.Utils; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.uri.BitcoinURIParseException; import org.slf4j.Logger; @@ -7,12 +8,17 @@ import org.slf4j.LoggerFactory; import org.xbill.DNS.*; import org.xbill.DNS.Record; import org.xbill.DNS.dnssec.ValidatingResolver; +import org.xbill.DNS.lookup.LookupResult; +import org.xbill.DNS.lookup.LookupSession; +import org.xbill.DNS.lookup.NoSuchDomainException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.time.Clock; import java.time.Instant; import java.util.*; +import java.util.concurrent.ExecutionException; import static com.sparrowwallet.drongo.uri.BitcoinURI.BITCOIN_SCHEME; @@ -27,8 +33,13 @@ public class DnsPaymentResolver { private final String hrn; private final String domain; + private final Clock clock; public DnsPaymentResolver(String hrn) { + this(hrn, Clock.systemUTC()); + } + + public DnsPaymentResolver(String hrn, Clock clock) { if(!StandardCharsets.US_ASCII.newEncoder().canEncode(hrn)) { throw new IllegalArgumentException("Invalid HRN containing non-ASCII characters: " + hrn); } @@ -38,9 +49,10 @@ public class DnsPaymentResolver { throw new IllegalArgumentException("Invalid HRN: " + hrn); } this.domain = parts[0] + ".user._bitcoin-payment." + parts[1]; + this.clock = clock; } - public Optional resolve() throws IOException, DnsPaymentValidationException, BitcoinURIParseException { + public Optional resolve() throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException { return resolve(DEFAULT_RESOLVER_IP_ADDRESS); } @@ -53,32 +65,40 @@ public class DnsPaymentResolver { * @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure * @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI */ - public Optional resolve(String resolverIpAddress) throws IOException, DnsPaymentValidationException, BitcoinURIParseException { + public Optional resolve(String resolverIpAddress) throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException { log.debug("Resolving payment record for: " + domain); PersistingResolver persistingResolver = new PersistingResolver(resolverIpAddress); - ValidatingResolver resolver = new ValidatingResolver(persistingResolver); - resolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII))); - resolver.setEDNS(0, 0, ExtendedFlags.DO); + ValidatingResolver validatingResolver = new ValidatingResolver(persistingResolver, clock); + validatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII))); + validatingResolver.setEDNS(0, 0, ExtendedFlags.DO); + AuthenticatingResolver authenticatingResolver = new AuthenticatingResolver(validatingResolver); - Lookup lookup = new Lookup(domain, Type.TXT); - lookup.setResolver(resolver); + try { + LookupSession lookupSession = LookupSession.builder().resolver(authenticatingResolver).build(); + LookupResult result = lookupSession.lookupAsync(getName(), Type.TXT, DClass.IN).toCompletableFuture().get(); + if(result.getRecords().isEmpty()) { + return Optional.empty(); + } - Message query = getQuery(); - Message response = resolver.send(query); - if(response.getSection(Section.ANSWER).isEmpty()) { - return Optional.empty(); + String strBitcoinUri = getBitcoinURI(result.getRecords()); + if(strBitcoinUri.isEmpty()) { + return Optional.empty(); + } + BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri); + validateResponse(authenticatingResolver, new ArrayList<>(persistingResolver.getChain())); + + byte[] proofChain = persistingResolver.chainToWire(); + log.debug("Resolved " + hrn + " with proof " + Utils.bytesToHex(proofChain)); + + return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain)); + } catch(ExecutionException e) { + if(e.getCause() instanceof NoSuchDomainException) { + return Optional.empty(); + } else { + throw e; + } } - - checkResponse(response, new ArrayList<>(persistingResolver.getChain())); - String strBitcoinUri = getBitcoinURI(response.getSection(Section.ANSWER)); - if(strBitcoinUri.isEmpty()) { - return Optional.empty(); - } - BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri); - validateResponse(response, new ArrayList<>(persistingResolver.getChain())); - - return Optional.of(new DnsPayment(hrn, bitcoinURI, persistingResolver.chainToWire())); } /** @@ -90,12 +110,13 @@ public class DnsPaymentResolver { * @throws DnsPaymentValidationException Thrown for a DNSSEC or BIP 353 validation failure * @throws BitcoinURIParseException Thrown for an invalid BIP 21 URI */ - public Optional resolve(byte[] proofChain) throws IOException, DnsPaymentValidationException, BitcoinURIParseException { + public Optional resolve(byte[] proofChain) throws IOException, DnsPaymentValidationException, BitcoinURIParseException, ExecutionException, InterruptedException { OfflineResolver offlineResolver = new OfflineResolver(proofChain); - ValidatingResolver offlineValidatingResolver = new ValidatingResolver(offlineResolver); + ValidatingResolver offlineValidatingResolver = new ValidatingResolver(offlineResolver, clock); offlineValidatingResolver.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII))); + AuthenticatingResolver authenticatingResolver = new AuthenticatingResolver(offlineValidatingResolver); - Instant now = Instant.now(); + Instant now = clock.instant(); Instant oneHourAgo = now.minusSeconds(3600); for(Record record : offlineResolver.getCachedSigs()) { if(record instanceof RRSIGRecord rrsig) { @@ -107,44 +128,36 @@ public class DnsPaymentResolver { } } - Message query = getQuery(); - Message offlineResponse = offlineValidatingResolver.send(query); - if(offlineResponse.getSection(Section.ANSWER).isEmpty()) { - return Optional.empty(); - } - - checkResponse(offlineResponse, offlineResolver.getRecords()); - String strBitcoinUri = getBitcoinURI(offlineResponse.getSection(Section.ANSWER)); - if(strBitcoinUri.isEmpty()) { - throw new BitcoinURIParseException("The DNS record for " + hrn + " did not contain a Bitcoin URI"); - } - BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri); - validateResponse(offlineResponse, offlineResolver.getRecords()); - - return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain)); - } - - private Message getQuery() throws TextParseException { - Name queryName = Name.fromString(domain + "."); - Record question = Record.newRecord(queryName, Type.TXT, DClass.IN); - return Message.newQuery(question); - } - - private void checkResponse(Message response, List records) throws DnsPaymentValidationException { - if(response.getRcode() != Rcode.NOERROR) { - StringBuilder reason = new StringBuilder(); - for(RRset set : response.getSectionRRsets(Section.ADDITIONAL)) { - if(set.getName().equals(Name.root) && set.getType() == Type.TXT && set.getDClass() == ValidatingResolver.VALIDATION_REASON_QCLASS) { - reason.append(((TXTRecord) set.first()).getStrings().getFirst()); - } + try { + LookupSession lookupSession = LookupSession.builder().resolver(authenticatingResolver).build(); + LookupResult result = lookupSession.lookupAsync(getName(), Type.TXT, DClass.IN).toCompletableFuture().get(); + if(result.getRecords().isEmpty()) { + return Optional.empty(); } - throw new DnsPaymentValidationException("DNS query for " + domain + " failed, " + (reason.isEmpty() ? "rcode was " + response.getRcode() : reason.toString())); + String strBitcoinUri = getBitcoinURI(result.getRecords()); + if(strBitcoinUri.isEmpty()) { + return Optional.empty(); + } + BitcoinURI bitcoinURI = new BitcoinURI(strBitcoinUri); + validateResponse(authenticatingResolver, offlineResolver.getRecords()); + + return Optional.of(new DnsPayment(hrn, bitcoinURI, proofChain)); + } catch(ExecutionException e) { + if(e.getCause() instanceof NoSuchDomainException) { + return Optional.empty(); + } else { + throw e; + } } } - private void validateResponse(Message response, List records) throws DnsPaymentValidationException { - boolean isValidated = response.getHeader().getFlag(Flags.AD); + private Name getName() throws TextParseException { + return Name.fromString(domain + "."); + } + + private void validateResponse(AuthenticatingResolver resolver, List records) throws DnsPaymentValidationException { + boolean isValidated = resolver.isAuthenticated(); if(!isValidated) { throw new DnsPaymentValidationException("DNSSEC validation failed, could not authenticate the payment instruction"); } diff --git a/src/main/java/com/sparrowwallet/drongo/dns/OfflineResolver.java b/src/main/java/com/sparrowwallet/drongo/dns/OfflineResolver.java index ce7e113..4eaca0d 100644 --- a/src/main/java/com/sparrowwallet/drongo/dns/OfflineResolver.java +++ b/src/main/java/com/sparrowwallet/drongo/dns/OfflineResolver.java @@ -58,32 +58,80 @@ public class OfflineResolver implements Resolver { @Override public CompletionStage sendAsync(Message query, Executor executor) { - Record question = query.getQuestion(); - List records = new ArrayList<>(); + Message response = makeNoErrorResponse(query); + addRecords(query.getQuestion(), response); - for(Record it : cachedRrs) { - if(it.getName().equals(question.getName()) && it.getType() == question.getType() && it.getDClass() == question.getDClass()) { - records.add(it); - } - } - - for(RRSIGRecord it : cachedSigs) { - if(it.getName().equals(question.getName()) && it.getTypeCovered() == question.getType() && it.getDClass() == question.getDClass()) { - records.add(it); - } - } - - Message response; - if(records.isEmpty()) { - response = makeEmptyResponse(query); - } else { - response = makeResponseForRecords(records, query); + if(response.getSection(Section.ANSWER).isEmpty() && response.getSection(Section.AUTHORITY).isEmpty()) { + response = makeNoDomainResponse(query); } return CompletableFuture.completedFuture(response); } - private Message makeEmptyResponse(Message query) { + private void addRecords(Record question, Message response) { + Name name = question.getName(); + + RRset cnameSet = getRRSet(name, Type.CNAME, question.getDClass()); + addRRSetToMessage(response, cnameSet); + + if(!cnameSet.rrs().isEmpty() && cnameSet.rrs().getFirst() instanceof CNAMERecord cnameRecord) { + name = cnameRecord.getTarget(); + } + + RRset answerSet = getRRSet(name, question.getType(), question.getDClass()); + addRRSetToMessage(response, answerSet); + } + + private void addRRSetToMessage(Message response, RRset rrset) { + rrset.rrs().stream().forEach(it -> response.addRecord(it, Section.ANSWER)); + rrset.sigs().stream().forEach(it -> response.addRecord(it, Section.ANSWER)); + + if(!rrset.sigs().isEmpty()) { + Name wildcard = RecordUtils.rrsetWildcard(rrset); + if(wildcard != null) { + RRset nsecRRset = getNSecRRSetForWildcard(wildcard); + nsecRRset.rrs().stream().forEach(it -> response.addRecord(it, Section.AUTHORITY)); + nsecRRset.sigs().stream().forEach(it -> response.addRecord(it, Section.AUTHORITY)); + } + } + } + + private RRset getRRSet(Name name, int type, int dclass) { + RRset rrset = new RRset(); + for(Record it : cachedRrs) { + if(it.getName().equals(name) && it.getType() == type && it.getDClass() == dclass) { + rrset.addRR(it); + } + } + + for(RRSIGRecord it : cachedSigs) { + if(it.getName().equals(name) && it.getTypeCovered() == type && it.getDClass() == dclass) { + rrset.addRR(it); + } + } + + return rrset; + } + + private RRset getNSecRRSetForWildcard(Name wildcard) { + RRset rrset = new RRset(); + + for(Record it : cachedRrs) { + if((it.getType() == Type.NSEC || it.getType() == Type.NSEC3) && RecordUtils.longestCommonName(it.getName(), wildcard) != Name.root) { + rrset.addRR(it); + } + } + + for(RRSIGRecord it : cachedSigs) { + if((it.getTypeCovered() == Type.NSEC || it.getTypeCovered() == Type.NSEC3) && RecordUtils.longestCommonName(it.getName(), wildcard) != Name.root) { + rrset.addRR(it); + } + } + + return rrset; + } + + private Message makeNoDomainResponse(Message query) { Header messageHeader = new Header(); messageHeader.setID(query.getHeader().getID()); messageHeader.setRcode(Rcode.NXDOMAIN); @@ -95,12 +143,14 @@ public class OfflineResolver implements Resolver { Message answerMessage = new Message(); answerMessage.setHeader(messageHeader); + for(Record record : query.getSection(Section.QUESTION)) { + answerMessage.addRecord(record, Section.QUESTION); + } + return answerMessage; } - private Message makeResponseForRecords(List records, Message query) { - Message answerMessage = new Message(); - + private Message makeNoErrorResponse(Message query) { Header messageHeader = new Header(); messageHeader.setID(query.getHeader().getID()); messageHeader.setRcode(Rcode.NOERROR); @@ -108,16 +158,14 @@ public class OfflineResolver implements Resolver { messageHeader.setFlag(Flags.CD); messageHeader.setFlag(Flags.RD); messageHeader.setFlag(Flags.RA); + + Message answerMessage = new Message(); answerMessage.setHeader(messageHeader); for(Record record : query.getSection(Section.QUESTION)) { answerMessage.addRecord(record, Section.QUESTION); } - for(Record record : records) { - answerMessage.addRecord(record, Section.ANSWER); - } - return answerMessage; } diff --git a/src/main/java/com/sparrowwallet/drongo/dns/PersistingResolver.java b/src/main/java/com/sparrowwallet/drongo/dns/PersistingResolver.java index 9e9f399..3866b71 100644 --- a/src/main/java/com/sparrowwallet/drongo/dns/PersistingResolver.java +++ b/src/main/java/com/sparrowwallet/drongo/dns/PersistingResolver.java @@ -1,5 +1,7 @@ package com.sparrowwallet.drongo.dns; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.xbill.DNS.*; import org.xbill.DNS.Record; @@ -10,6 +12,8 @@ import java.util.concurrent.CompletionStage; import java.util.concurrent.Executor; public class PersistingResolver extends SimpleResolver { + private static final Logger log = LoggerFactory.getLogger(PersistingResolver.class); + private final Set chain = new LinkedHashSet<>(); public PersistingResolver(String hostname) throws UnknownHostException { @@ -20,6 +24,10 @@ public class PersistingResolver extends SimpleResolver { public CompletionStage sendAsync(Message query, Executor executor) { CompletionStage result = super.sendAsync(query, executor); return result.thenApply(response -> { + if(log.isDebugEnabled()) { + log.debug(responseToString(query, response)); + } + addAnswerSectionToChain(response.getSection(Section.ANSWER)); addAuthoritySectionToChain(response.getSection(Section.AUTHORITY)); return response; @@ -35,7 +43,7 @@ public class PersistingResolver extends SimpleResolver { private void addAuthoritySectionToChain(List section) { if(section != null) { for(Record r : section) { - if((r.getType() == Type.RRSIG && r.getRRsetType() == Type.NSEC && r.getRRsetType() == Type.NSEC3)|| r.getType() == Type.NSEC || r.getType() == Type.NSEC3) { + if((r.getType() == Type.RRSIG && (r.getRRsetType() == Type.NSEC || r.getRRsetType() == Type.NSEC3)) || r.getType() == Type.NSEC || r.getType() == Type.NSEC3) { chain.add(r); } } @@ -57,4 +65,15 @@ public class PersistingResolver extends SimpleResolver { return baos.toByteArray(); } + + private static String responseToString(Message query, Message response) { + StringBuilder sb = new StringBuilder(); + sb.append("Query for ").append(query.getQuestion().getName()).append(" returned:\n"); + sb.append("Answer section:\n"); + response.getSection(Section.ANSWER).stream().forEach(rr -> sb.append(rr).append("\n")); + sb.append("Authority section:\n"); + response.getSection(Section.AUTHORITY).stream().forEach(rr -> sb.append(rr).append("\n")); + sb.append("\n"); + return sb.toString(); + } } diff --git a/src/main/java/com/sparrowwallet/drongo/dns/RecordUtils.java b/src/main/java/com/sparrowwallet/drongo/dns/RecordUtils.java index 0b0fcf2..7c01b92 100644 --- a/src/main/java/com/sparrowwallet/drongo/dns/RecordUtils.java +++ b/src/main/java/com/sparrowwallet/drongo/dns/RecordUtils.java @@ -63,4 +63,61 @@ public class RecordUtils { return warnings; } + + /** + * Determine by looking at a signed RRset whether the RRset name was the result of a wildcard + * expansion. If so, return the name of the generating wildcard. + * + * @param rrset The rrset to chedck. + * @return the wildcard name, if the rrset was synthesized from a wildcard. null if not. + */ + public static Name rrsetWildcard(RRset rrset) { + List sigs = rrset.sigs(); + RRSIGRecord firstSig = sigs.getFirst(); + + // check rest of signatures have identical label count + for(int i = 1; i < sigs.size(); i++) { + if(sigs.get(i).getLabels() != firstSig.getLabels()) { + throw new IllegalArgumentException("Label count mismatch on RRSIGs"); + } + } + + // if the RRSIG label count is shorter than the number of actual labels, + // then this rrset was synthesized from a wildcard. + // Note that the RRSIG label count doesn't count the root label. + Name wn = rrset.getName(); + + // skip a leading wildcard label in the dname (RFC4035 2.2) + if(rrset.getName().isWild()) { + wn = new Name(wn, 1); + } + + int labelDiff = (wn.labels() - 1) - firstSig.getLabels(); + if(labelDiff > 0) { + return wn.wild(labelDiff); + } + + return null; + } + + /** + * Finds the longest domain name in common with the given name. + * + * @param domain1 The first domain to process. + * @param domain2 The second domain to process. + * @return The longest label in common of domain1 and domain2. The least common name is the root. + */ + public static Name longestCommonName(Name domain1, Name domain2) { + int l = Math.min(domain1.labels(), domain2.labels()); + domain1 = new Name(domain1, domain1.labels() - l); + domain2 = new Name(domain2, domain2.labels() - l); + for(int i = 0; i < l - 1; i++) { + Name ns1 = new Name(domain1, i); + if(ns1.equals(new Name(domain2, i))) { + return ns1; + } + } + + return Name.root; + } } diff --git a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java index 94a49c0..484d896 100644 --- a/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java +++ b/src/main/java/com/sparrowwallet/drongo/psbt/PSBTOutput.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.*; +import java.util.concurrent.ExecutionException; import static com.sparrowwallet.drongo.protocol.ScriptType.*; import static com.sparrowwallet.drongo.psbt.PSBTEntry.*; @@ -294,7 +295,7 @@ public class PSBTOutput { return dnssecProof; } - public Optional getDnsPayment() throws DnsPaymentValidationException, IOException, BitcoinURIParseException { + public Optional getDnsPayment() throws DnsPaymentValidationException, IOException, BitcoinURIParseException, ExecutionException, InterruptedException { if(dnssecProof == null || dnssecProof.isEmpty()) { return Optional.empty(); } diff --git a/src/test/java/com/sparrowwallet/drongo/dns/DnsPaymentResolverTest.java b/src/test/java/com/sparrowwallet/drongo/dns/DnsPaymentResolverTest.java index d3401c5..00ccac2 100644 --- a/src/test/java/com/sparrowwallet/drongo/dns/DnsPaymentResolverTest.java +++ b/src/test/java/com/sparrowwallet/drongo/dns/DnsPaymentResolverTest.java @@ -6,13 +6,18 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.io.IOException; +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; import java.util.Optional; +import java.util.concurrent.ExecutionException; public class DnsPaymentResolverTest { @Test - public void resolverTest() throws DnsPaymentValidationException, IOException, BitcoinURIParseException { - byte[] proofChain = Utils.hexToBytes("00002e000100013ccb0113003008000002a30068993280687d83004f66005a48a604886288cc78c2a35e48816b7a182a349f397f2cd4c1bfa6de634acc9b9b0d2236fd8f257fa8641ae46da7ca17a697c965beabb5477ea6d0cc198b77c8cb9398f8f6fd36c7dc32439409625209b7c3d12108f2c973ea735f764ee629135ed67f016e63949a84e1f120b5146a27221180a0fbd0d632cc900c488b709260f2d479e6d787f2f9fa31222cacdbb696ddc3789744c691d27a8be4486fd7a74b51e417dfb9a9ba8f148f468c536debb4a7dc3803ea6213c55c3efd19cbf29059e5e460803e9656bdac7feacc38bf2bb8a9a3cbc5025841c1b71a58246cab007209bf2f22d4fdd4b80fe6d3bce9e5d2bb80df1949d62f09feb3a5bffe2a1bc6ab000030000100013ccb01080100030803010001b11b182a464c3adc6535aa59613bda7a61cac86945c20b773095941194f4b9f516e8bd924b1e50e3fe83918b51e54529d4e5a1e45303df8462241d5e05979979ae5bf9c6c598c08a496e17f3bd3732d5aebe62667b61db1bbe178f27ac99408165a230d6aee78348e6c67789541f845b2ada96667f8dd16ae44f9e260c4a138b3bb1015965ebe609434a06464bd7d29bac47c3017e83c0f89bca1a9e3bdd0813715f3484292df589bc632e27d37efc02837cb85d770d5bd53a36edc99a8294771aa93cf22406f5506c8cf850ed85c1a475dee5c2d3700b3f5631d903524b849995c20cb407ed411f70b428ae3d642716fe239335aa961a752e67fb6dca0bf729000030000100013ccb01080100030803010001b6aec4b48567e2925a2d9c4fa4c96e6dddf86215a9bd8dd579c38ccb1199ed1be89946a7f72fc2633909a2792d0eed1b5afb2ee4c78d865a76d6cd9369d999c96af6be0a2274b8f2e9e0a0065bd20257570f08bc14c16f5616426881a83dbce6926e391c138a2ec317efa7349264de2e791c9b7d4a6048ee6eedf27bf1ece398ff0d229f18377cb1f6b98d1228ef217b8146c0c73851b89a6fc37c621ca187e16428a743ffea0072e185ef93e39525cee3ad01e0c94d2e511c8c313322c29ab91631e1856049a36898684c3056e5997473816fb547acb0be6e660bdfa89a5cb28b3669d8625f3f018c7b3b8a4860e774ee8261811ce7f96c461bc162c1a374f3000030000100013ccb01080101030803010001acffb409bcc939f831f7a1e5ec88f7a59255ec53040be432027390a4ce896d6f9086f3c5e177fbfe118163aaec7af1462c47945944c4e2c026be5e98bbcded25978272e1e3e079c5094d573f0e83c92f02b32d3513b1550b826929c80dd0f92cac966d17769fd5867b647c3f38029abdc48152eb8f207159ecc5d232c7c1537c79f4b7ac28ff11682f21681bf6d6aba555032bf6f9f036beb2aaa5b3778d6eebfba6bf9ea191be4ab0caea759e2f773a1f9029c73ecb8d5735b9321db085f1b8e2d8038fe2941992548cee0d67dd4547e11dd63af9c9fc1c5466fb684cf009d7197c2cf79e792ab501e6a8a1ca519af2cb9b5f6367e94c0d47502451357be1b5000030000100013ccb01080101030803010001af7a8deba49d995a792aefc80263e991efdbc86138a931deb2c65d5682eab5d3b03738e3dfdc89d96da64c86c0224d9ce02514d285da3068b19054e5e787b2969058e98e12566c8c808c40c0b769e1db1a24a1bd9b31e303184a31fc7bb56b85bbba8abc02cd5040a444a36d47695969849e16ad856bb58e8fac8855224400319bdab224d83fc0e66aab32ff74bfeaf0f91c454e6850a1295207bbd4cdde8f6ffb08faa9755c2e3284efa01f99393e18786cb132f1e66ebc6517318e1ce8a3b7337ebb54d035ab57d9706ecd9350d4afacd825e43c8668eece89819caf6817af62dc4fbd82f0e33f6647b2b6bda175f14607f59f4635451e6b27df282ef73d8703636f6d00002b00010000ba3800244d060d028acbb0cd28f41250a80a491389424d341522d946b0da0c0291f2d3d771d7805a03636f6d00002e00010000ba380113002b08010001518068911140687fdfb0b569006748fcc8daa42fbdbe3e9103f4c3ae041ac72e5a1ffd161004fbf2f1b2b30f82fe31e927371b70866108bd55acab8a5afa84fa3a232518b0e952af3dfad9559bcb4e82b63fbb4f8542a5ebd5d047dad5b2024bde85c4fc4205a0984735ae96cba6d6f7bace837d40b0bf7351ead906105c9459fba5e9c6cad5068b4f91908f601eabea293be0de72f2c06e9392ba60c652ebdb3ce701d547f8c31fb23a2bc44a733dbeb1af210f758cdd4c8d748522c5f074615b750ec657c168f47e42af3292325506ed16c8020f83670891c3ca7bbbdd8c467f25e0e4644a3f9e1674922819726823ca7f183d1346840899cdbe7314a2336926940ea036f6b278b7a7ada7b003636f6d00002e000100002424005700300d010001518068920efb687e474f4d0603636f6d00b2e671a909bab6910567084b8347cb199b924a4acf9e1a2602ba0adaa3b056890609bd88ee767161672bbe89466e2c035c0bce3a755f33b910047fa27a90b9c203636f6d00003000010000242400440100030df17b60fb56d522f8634153e785c0a978532ea76de39d34bb356d3d042e07f29f7b992176cc83acef7b78ea750425203b18f2b8228b3cdd2bc7eb13c3e3035a7e03636f6d00003000010000242400440101030db71f0465101ddbe2bf0c9455d12fa16c1cda44f4bf1ba2553418ad1f3aa9b06973f21b84eb532cf4035ee8d4832ca26d89306a7d32560c0cb0129d450ac108350d73706172726f7777616c6c657403636f6d00002b00010000546000243dc40d0256040d991c1075c4a8555445f9a5ce52ce6801aaf45d3e87663e7fbd68bc312b0d73706172726f7777616c6c657403636f6d00002b0001000054600024cf3b0d02656da59836422f5e198e73fc35e6a89bc0838deaac565e71a19804fc1250e4ce0d73706172726f7777616c6c657403636f6d00002e0001000054600057002b0d020001518068876196687e16ae504103636f6d00c23f50cb0c009ffaf23dfb105a24110c95a41de00c100bc35305c870fcfa6d6ed94581f64199c9328f748a1f04889c92c7122f4e0d6611e8653a608705348fc80d73706172726f7777616c6c657403636f6d00002e000100000a80006500300d0200000e106891efe0687e29603dc40d73706172726f7777616c6c657403636f6d002b96ad4cdc8619f89d74317373ff0b40b9de3132cf957ee57c653c204d1d3611d6264d6baefb1c45c1fe2d499cc77587183f4900a1f0512b0478a60e4944c0410d73706172726f7777616c6c657403636f6d000030000100000a8000440100030d24c8364b3f942b0062f1c63880b959b2e7827f1cffff8d5e38f7fde1b22d621d1c4a0cd9a9b0c6c70b1c94543ccdc5502481aebd6e2b44656c9ea339ac81e83b0d73706172726f7777616c6c657403636f6d000030000100000a8000440100030d95676c7b25e7794a8a7e4b19ed638e47aca735d02ce2dd08b2886c20c31a2cb9e7cc8b85023a46eeb637020119dcaa6bbc0747e12340fa813199799de579de8a0d73706172726f7777616c6c657403636f6d000030000100000a8000440101030db0372521337fd56d8b62e917b7866b7faa753d25322e12b52a3eb5ff9f4c9f66227f508fe33ba139f2f1354fe3ded6d3da76d49be926198dc2940f2c5282c7fe0d73706172726f7777616c6c657403636f6d000030000100000a8000440101030dddf917743f320a49f6218d706218b6cae574f1db7688555e0d5f0455405d6865993f0147fb4b33baa207b28d232c9e70419ddcae72050311098cd4cfaa07969b0563726169670475736572105f626974636f696e2d7061796d656e740d73706172726f7777616c6c657403636f6d0000100001000002fe003332626974636f696e3a6263317177746865343378657561736b6c636c71346b766872656c75763368753932727a656a34326a730563726169670475736572105f626974636f696e2d7061796d656e740d73706172726f7777616c6c657403636f6d00002e0001000002fe006500100d0500000e106891efe0687e2960bb260d73706172726f7777616c6c657403636f6d00e7ae93d23b747737554c4d52dd1ec0f58c411c6a474da46c3c24d0db970d86e91bf91b5eabeb1ed59121678ef534a25a6f75ce0588e6524439c11d208f301d46"); - DnsPaymentResolver resolver = new DnsPaymentResolver("craig@sparrowwallet.com"); + public void resolverTest() throws DnsPaymentValidationException, IOException, BitcoinURIParseException, ExecutionException, InterruptedException { + Clock clock = Clock.fixed(Instant.ofEpochSecond(1753784347L), ZoneId.of("UTC")); + byte[] proofChain = Utils.hexToBytes("00002e000100002bb30113003008000002a30068993280687d83004f66005a48a604886288cc78c2a35e48816b7a182a349f397f2cd4c1bfa6de634acc9b9b0d2236fd8f257fa8641ae46da7ca17a697c965beabb5477ea6d0cc198b77c8cb9398f8f6fd36c7dc32439409625209b7c3d12108f2c973ea735f764ee629135ed67f016e63949a84e1f120b5146a27221180a0fbd0d632cc900c488b709260f2d479e6d787f2f9fa31222cacdbb696ddc3789744c691d27a8be4486fd7a74b51e417dfb9a9ba8f148f468c536debb4a7dc3803ea6213c55c3efd19cbf29059e5e460803e9656bdac7feacc38bf2bb8a9a3cbc5025841c1b71a58246cab007209bf2f22d4fdd4b80fe6d3bce9e5d2bb80df1949d62f09feb3a5bffe2a1bc6ab000030000100002bb301080100030803010001b11b182a464c3adc6535aa59613bda7a61cac86945c20b773095941194f4b9f516e8bd924b1e50e3fe83918b51e54529d4e5a1e45303df8462241d5e05979979ae5bf9c6c598c08a496e17f3bd3732d5aebe62667b61db1bbe178f27ac99408165a230d6aee78348e6c67789541f845b2ada96667f8dd16ae44f9e260c4a138b3bb1015965ebe609434a06464bd7d29bac47c3017e83c0f89bca1a9e3bdd0813715f3484292df589bc632e27d37efc02837cb85d770d5bd53a36edc99a8294771aa93cf22406f5506c8cf850ed85c1a475dee5c2d3700b3f5631d903524b849995c20cb407ed411f70b428ae3d642716fe239335aa961a752e67fb6dca0bf729000030000100002bb301080100030803010001b6aec4b48567e2925a2d9c4fa4c96e6dddf86215a9bd8dd579c38ccb1199ed1be89946a7f72fc2633909a2792d0eed1b5afb2ee4c78d865a76d6cd9369d999c96af6be0a2274b8f2e9e0a0065bd20257570f08bc14c16f5616426881a83dbce6926e391c138a2ec317efa7349264de2e791c9b7d4a6048ee6eedf27bf1ece398ff0d229f18377cb1f6b98d1228ef217b8146c0c73851b89a6fc37c621ca187e16428a743ffea0072e185ef93e39525cee3ad01e0c94d2e511c8c313322c29ab91631e1856049a36898684c3056e5997473816fb547acb0be6e660bdfa89a5cb28b3669d8625f3f018c7b3b8a4860e774ee8261811ce7f96c461bc162c1a374f3000030000100002bb301080101030803010001acffb409bcc939f831f7a1e5ec88f7a59255ec53040be432027390a4ce896d6f9086f3c5e177fbfe118163aaec7af1462c47945944c4e2c026be5e98bbcded25978272e1e3e079c5094d573f0e83c92f02b32d3513b1550b826929c80dd0f92cac966d17769fd5867b647c3f38029abdc48152eb8f207159ecc5d232c7c1537c79f4b7ac28ff11682f21681bf6d6aba555032bf6f9f036beb2aaa5b3778d6eebfba6bf9ea191be4ab0caea759e2f773a1f9029c73ecb8d5735b9321db085f1b8e2d8038fe2941992548cee0d67dd4547e11dd63af9c9fc1c5466fb684cf009d7197c2cf79e792ab501e6a8a1ca519af2cb9b5f6367e94c0d47502451357be1b5000030000100002bb301080101030803010001af7a8deba49d995a792aefc80263e991efdbc86138a931deb2c65d5682eab5d3b03738e3dfdc89d96da64c86c0224d9ce02514d285da3068b19054e5e787b2969058e98e12566c8c808c40c0b769e1db1a24a1bd9b31e303184a31fc7bb56b85bbba8abc02cd5040a444a36d47695969849e16ad856bb58e8fac8855224400319bdab224d83fc0e66aab32ff74bfeaf0f91c454e6850a1295207bbd4cdde8f6ffb08faa9755c2e3284efa01f99393e18786cb132f1e66ebc6517318e1ce8a3b7337ebb54d035ab57d9706ecd9350d4afacd825e43c8668eece89819caf6817af62dc4fbd82f0e33f6647b2b6bda175f14607f59f4635451e6b27df282ef73d8703636f6d00002b00010001474a00244d060d028acbb0cd28f41250a80a491389424d341522d946b0da0c0291f2d3d771d7805a03636f6d00002e00010001474a0113002b080100015180689978d068884740b569009f50ff461b38abba3d30ace990cb95740c3faae42082ef882a551c6b4a30d2b29caa59b0556ea80efc4a6bc126ba77d86d7e926fb741380018d038935154e0ec37485a479d8d3a5b5d79f15c7be24d5c46b58581b8a6dd55f44de72d20a3b232134a18fcfb14123c94d6d0cb249f90e56439e84df2cbae4da72491ba54b9ca2e8436fbdbb9591bdd93ce0411cf35002bc24c376526ed1711743d38ca227915f6e5e3e4c314617ad0d4e038646e885800e8853a79b7a160b5375bf492c19a7f5752718f11116b9a3278eaf19f34ee597fe315eaba1ce86c52625e4dfdcacc8d04994ee7600c4ec51357a2c23e936b05399153df6a31edc4e3d507976904dea64403636f6d00002e0001000006a4005700300d010001518068920efb687e474f4d0603636f6d00b2e671a909bab6910567084b8347cb199b924a4acf9e1a2602ba0adaa3b056890609bd88ee767161672bbe89466e2c035c0bce3a755f33b910047fa27a90b9c203636f6d0000300001000006a400440100030df17b60fb56d522f8634153e785c0a978532ea76de39d34bb356d3d042e07f29f7b992176cc83acef7b78ea750425203b18f2b8228b3cdd2bc7eb13c3e3035a7e03636f6d0000300001000006a400440101030db71f0465101ddbe2bf0c9455d12fa16c1cda44f4bf1ba2553418ad1f3aa9b06973f21b84eb532cf4035ee8d4832ca26d89306a7d32560c0cb0129d450ac108350d73706172726f7777616c6c657403636f6d00002b00010000546000243dc40d0256040d991c1075c4a8555445f9a5ce52ce6801aaf45d3e87663e7fbd68bc312b0d73706172726f7777616c6c657403636f6d00002b0001000054600024cf3b0d02656da59836422f5e198e73fc35e6a89bc0838deaac565e71a19804fc1250e4ce0d73706172726f7777616c6c657403636f6d00002e0001000054600057002b0d020001518068901cfc6886d214504103636f6d00d763f6d6ecb0f5e5b982f845d5fd5846ee9ce3a4a8cca71e8b7525476d6b6d2a3e196730e8b6bcfbe6774dd204519e609aa708f6151fe0247ccde98d2bd8c55d0d73706172726f7777616c6c657403636f6d00002e0001000007e5006500300d0200000e106891efe0687e29603dc40d73706172726f7777616c6c657403636f6d002b96ad4cdc8619f89d74317373ff0b40b9de3132cf957ee57c653c204d1d3611d6264d6baefb1c45c1fe2d499cc77587183f4900a1f0512b0478a60e4944c0410d73706172726f7777616c6c657403636f6d0000300001000007e500440100030d24c8364b3f942b0062f1c63880b959b2e7827f1cffff8d5e38f7fde1b22d621d1c4a0cd9a9b0c6c70b1c94543ccdc5502481aebd6e2b44656c9ea339ac81e83b0d73706172726f7777616c6c657403636f6d0000300001000007e500440100030d95676c7b25e7794a8a7e4b19ed638e47aca735d02ce2dd08b2886c20c31a2cb9e7cc8b85023a46eeb637020119dcaa6bbc0747e12340fa813199799de579de8a0d73706172726f7777616c6c657403636f6d0000300001000007e500440101030db0372521337fd56d8b62e917b7866b7faa753d25322e12b52a3eb5ff9f4c9f66227f508fe33ba139f2f1354fe3ded6d3da76d49be926198dc2940f2c5282c7fe0d73706172726f7777616c6c657403636f6d0000300001000007e500440101030dddf917743f320a49f6218d706218b6cae574f1db7688555e0d5f0455405d6865993f0147fb4b33baa207b28d232c9e70419ddcae72050311098cd4cfaa07969b0563726169670475736572105f626974636f696e2d7061796d656e740d73706172726f7777616c6c657403636f6d000010000100000e10003332626974636f696e3a6263317177746865343378657561736b6c636c71346b766872656c75763368753932727a656a34326a730563726169670475736572105f626974636f696e2d7061796d656e740d73706172726f7777616c6c657403636f6d00002e000100000e10006500100d0500000e106891efe0687e2960bb260d73706172726f7777616c6c657403636f6d00e7ae93d23b747737554c4d52dd1ec0f58c411c6a474da46c3c24d0db970d86e91bf91b5eabeb1ed59121678ef534a25a6f75ce0588e6524439c11d208f301d46"); + DnsPaymentResolver resolver = new DnsPaymentResolver("craig@sparrowwallet.com", clock); Optional dnsPayment = resolver.resolve(proofChain); if(dnsPayment.isPresent()) { Assertions.assertEquals(dnsPayment.get().bitcoinURI().getAddress().toString(), "bc1qwthe43xeuasklclq4kvhreluv3hu92rzej42js"); @@ -20,4 +25,30 @@ public class DnsPaymentResolverTest { Assertions.fail("Could not resolve proof chain"); } } + + @Test + public void cnameResolverTest() throws DnsPaymentValidationException, IOException, BitcoinURIParseException, ExecutionException, InterruptedException { + Clock clock = Clock.fixed(Instant.ofEpochSecond(1753784347L), ZoneId.of("UTC")); + byte[] proofChain = Utils.hexToBytes("00002e0001000149d60113003008000002a30068993280687d83004f66005a48a604886288cc78c2a35e48816b7a182a349f397f2cd4c1bfa6de634acc9b9b0d2236fd8f257fa8641ae46da7ca17a697c965beabb5477ea6d0cc198b77c8cb9398f8f6fd36c7dc32439409625209b7c3d12108f2c973ea735f764ee629135ed67f016e63949a84e1f120b5146a27221180a0fbd0d632cc900c488b709260f2d479e6d787f2f9fa31222cacdbb696ddc3789744c691d27a8be4486fd7a74b51e417dfb9a9ba8f148f468c536debb4a7dc3803ea6213c55c3efd19cbf29059e5e460803e9656bdac7feacc38bf2bb8a9a3cbc5025841c1b71a58246cab007209bf2f22d4fdd4b80fe6d3bce9e5d2bb80df1949d62f09feb3a5bffe2a1bc6ab0000300001000149d601080100030803010001b11b182a464c3adc6535aa59613bda7a61cac86945c20b773095941194f4b9f516e8bd924b1e50e3fe83918b51e54529d4e5a1e45303df8462241d5e05979979ae5bf9c6c598c08a496e17f3bd3732d5aebe62667b61db1bbe178f27ac99408165a230d6aee78348e6c67789541f845b2ada96667f8dd16ae44f9e260c4a138b3bb1015965ebe609434a06464bd7d29bac47c3017e83c0f89bca1a9e3bdd0813715f3484292df589bc632e27d37efc02837cb85d770d5bd53a36edc99a8294771aa93cf22406f5506c8cf850ed85c1a475dee5c2d3700b3f5631d903524b849995c20cb407ed411f70b428ae3d642716fe239335aa961a752e67fb6dca0bf7290000300001000149d601080100030803010001b6aec4b48567e2925a2d9c4fa4c96e6dddf86215a9bd8dd579c38ccb1199ed1be89946a7f72fc2633909a2792d0eed1b5afb2ee4c78d865a76d6cd9369d999c96af6be0a2274b8f2e9e0a0065bd20257570f08bc14c16f5616426881a83dbce6926e391c138a2ec317efa7349264de2e791c9b7d4a6048ee6eedf27bf1ece398ff0d229f18377cb1f6b98d1228ef217b8146c0c73851b89a6fc37c621ca187e16428a743ffea0072e185ef93e39525cee3ad01e0c94d2e511c8c313322c29ab91631e1856049a36898684c3056e5997473816fb547acb0be6e660bdfa89a5cb28b3669d8625f3f018c7b3b8a4860e774ee8261811ce7f96c461bc162c1a374f30000300001000149d601080101030803010001acffb409bcc939f831f7a1e5ec88f7a59255ec53040be432027390a4ce896d6f9086f3c5e177fbfe118163aaec7af1462c47945944c4e2c026be5e98bbcded25978272e1e3e079c5094d573f0e83c92f02b32d3513b1550b826929c80dd0f92cac966d17769fd5867b647c3f38029abdc48152eb8f207159ecc5d232c7c1537c79f4b7ac28ff11682f21681bf6d6aba555032bf6f9f036beb2aaa5b3778d6eebfba6bf9ea191be4ab0caea759e2f773a1f9029c73ecb8d5735b9321db085f1b8e2d8038fe2941992548cee0d67dd4547e11dd63af9c9fc1c5466fb684cf009d7197c2cf79e792ab501e6a8a1ca519af2cb9b5f6367e94c0d47502451357be1b50000300001000149d601080101030803010001af7a8deba49d995a792aefc80263e991efdbc86138a931deb2c65d5682eab5d3b03738e3dfdc89d96da64c86c0224d9ce02514d285da3068b19054e5e787b2969058e98e12566c8c808c40c0b769e1db1a24a1bd9b31e303184a31fc7bb56b85bbba8abc02cd5040a444a36d47695969849e16ad856bb58e8fac8855224400319bdab224d83fc0e66aab32ff74bfeaf0f91c454e6850a1295207bbd4cdde8f6ffb08faa9755c2e3284efa01f99393e18786cb132f1e66ebc6517318e1ce8a3b7337ebb54d035ab57d9706ecd9350d4afacd825e43c8668eece89819caf6817af62dc4fbd82f0e33f6647b2b6bda175f14607f59f4635451e6b27df282ef73d8703636f6d00002b000100007a7100244d060d028acbb0cd28f41250a80a491389424d341522d946b0da0c0291f2d3d771d7805a03636f6d00002e000100007a710113002b08010001518068977e9068864d00b569002d3014cdeea855ed967775aa53a2671069c3d5c3a47b4e51fd4926fda34c7f6bcf970272fe18c516cbfc0119b024badb77082f1b956d948f3fd92e05557835ca10379cf523583e137e4d1d35047c4ca07d1ff11708241a091a6167d4538c1a084de6be6360997767cad3d44e7530461c8e8c744bfc146c02b00360e29eb4b6d6ec8ff3a1fe8eb44e3098ecf865ddf8f19d66b99105d961218925cab2c01ff6ac3ac9d364089536b6e2e71d2ec063949eb71137fc646897113f92673c8a74c23bb66283c80bd26d9d70ef3fe9024ecf299895a1e856954e7a5b11f528a5953baf75f6d5db6ccbc5f031b667f612a6c7d27096e40e8bf7fb2fda709900bc221ee903636f6d00002e00010000171a005700300d010001518068920efb687e474f4d0603636f6d00b2e671a909bab6910567084b8347cb199b924a4acf9e1a2602ba0adaa3b056890609bd88ee767161672bbe89466e2c035c0bce3a755f33b910047fa27a90b9c203636f6d00003000010000171a00440100030df17b60fb56d522f8634153e785c0a978532ea76de39d34bb356d3d042e07f29f7b992176cc83acef7b78ea750425203b18f2b8228b3cdd2bc7eb13c3e3035a7e03636f6d00003000010000171a00440101030db71f0465101ddbe2bf0c9455d12fa16c1cda44f4bf1ba2553418ad1f3aa9b06973f21b84eb532cf4035ee8d4832ca26d89306a7d32560c0cb0129d450ac108350b6d617474636f72616c6c6f03636f6d00002b00010000546000249f000d02594d2813e04a1d2660ff3c0afc5579b9ec0fe72cc206dc6f248bbe6dd652e1950b6d617474636f72616c6c6f03636f6d00002b0001000054600024e2f50d02f0e161567d468087ff27b051abc94476178a7cb635da1aa705e05c77ca81de520b6d617474636f72616c6c6f03636f6d00002e0001000054600057002b0d020001518068900e906886c3a8504103636f6d006d674908febdc7e78aa921806332f66656388c9ecb0305391d70a251886654f841426c54969009b261b3ab5c9cb5f2f9ba94fe5327722c079e89fb1f33963c330b6d617474636f72616c6c6f03636f6d00002e00010000456b006300300d0200093a80689827d568859dbde2f50b6d617474636f72616c6c6f03636f6d00f8fcb4e5ea52960df7464a5dc487043b6fb8fb8a083393c18902c47bd0c536f2cf138da967bc8be8599ef28a6bb781ce95e4b9617d2c3dbc8c5029092d80c4bc0b6d617474636f72616c6c6f03636f6d00003000010000456b00440100030dfd9dbc34cb5053a2c4a6b3d0dc60fc65d8a992dc1e080f6deeddba7fe6b25217730de64c9a1ce986b3f81f556881fe0e7b5b20c8ae381c4fefdbc311aa7d22ee0b6d617474636f72616c6c6f03636f6d00003000010000456b00440101030dec7c1fa1752495c42d2224eace96ed74144e9cb811608dd91594974bdc723fdc5b38a37c3340f1deca68a7ec82248822954b2994de5ac99ff6e9db95fd42c94b046d6174740475736572105f626974636f696e2d7061796d656e740b6d617474636f72616c6c6f03636f6d000010000100000d8201ecff626974636f696e3a626331717a7477793678656e337a647474377a3076726761706d6a74667a3861636a6b6670356670376c3f6c6e6f3d6c6e6f317a7235717975677167736b726b37306b716d7571377633646e7232666e6d68756b7073396e386875743438766b7170716e736b743273767371776a616b70376b36707968746b7578773779326b716d73786c777275687a7176307a736e686839713374397868783339737563367173723037656b6d3565736479756d307736366d6e783876647175777670376470356a70376a337635637036616a3077333239666e6b7171763630713936737a356e6b726335723935716666783030327135337471646beb3878396d32746d7438356a74706d63796376666e727078336c723435683267376e6133736563377867756374667a7a636d386a6a71746a3579613237746536306a303376707430767139746d326e3979786c32686e67666e6d79676573613235733475347a6c78657771707670393478743772757234726878756e776b74686b39766c79336c6d356868307071763461796d6371656a6c6773736e6c707a776c6767796b6b616a7037796a73356a76723261676b79797063646c6a323830637934366a70796e73657a72636a326b7761326c797238787664366c666b706834787278746b327863336c7071046d6174740475736572105f626974636f696e2d7061796d656e740b6d617474636f72616c6c6f03636f6d00002e000100000d82006300100d0500000e106894172f68818d17a7150b6d617474636f72616c6c6f03636f6d00ba9ee201fe8e135c732d61f8ba32580a7a4e6f3e490cd520b7e4afe9004f257c1b986dd2ef8d4588f4d3810da04249c48b88a6c284f43be5703220ee2a955320056e696e6a6100002b0001000015640024b4020802c8f816a7a575bdb2f997f682aab2653ba2cb5eddb69b036a30742a33befaf141056e696e6a6100002e0001000015640113002b0801000151806896d5d06885a440b56900a41759b94a4adf6192a0fb6e0f3ee388c15cd5b4f80fe961b1efbe5f93c2941c41ed1b71e9cdb5ccd651ffaf4d3c3158b341f21ccfbdf99b80485ceae57641e094919cc5ffe219c4ee25e3aa6bd02ba378de69bda940da8d1a873942acc683b25f41641fbf922833311af6fba9443532a37fd601a8dfb000f5a749b5ece5c847bab87c770605d1b2fa5c5528a4c78388b0a99ff5ca49580777a3854f472b06aa28a1bc53d4dda596ca6df1275227a107e6520605c919fc7048081fd3d396784a49928a3f32f1445f5fa56d2a4be8c2f5f7da68deeb974e7023b507cffea1e69d6706a62560321fa3a7492256715b229dcc802f51321bbb201bf76571dfa55ef4056e696e6a6100002e000100000c9e01190030080100000e10689f57ec68839a5cb402056e696e6a61006e05a6c0c66f0da44f5905afbb29f819692ccc8e867b45c25839bc5b7ed203383d2df06a284b3414b71848a77bebeb209333c1aeb52700cf3e630232e29d4befe5e708a0fa5fed527e6977ff41607ec531c8aa55be8cfac4beb38fd08b73a01deb25dd1b046c1e27ea210f1e9198672e8931b1eeafa6b24355fbaeb336c86bfb455ce4eea1b60c7218b3e077930be6250d4f81c9b73d9cecf9126e6962dddd489674ae560dfc18e63ef2d6a71c8347dbdca986937cc9ff2f793c0ee196bbef70784ec2cb7261393e32ba31db67043dc418fd17a74800194e77ab88130fa5e9736acd63f0d6b32ebee665bf4d95344f1d71cda00b2de99ce2e3a52b8e61b2c413056e696e6a61000030000100000c9e0088010003080301000197edb59d4f181e2761dd8d0465854339afc71fd89e47155981ddd175cdce79477552aeaf7b5a08fc4ac6025555f60582f2060e630edfb35b9c7cc30990fb9c3dc9f2fd036c962f67b94c9670d4ceaacd77973bca82ab7c9615f7e4320dda5b6d74dec673017c6fa448b5542a804e08ac873c509c1945ff734c320491e4b18e6d056e696e6a61000030000100000c9e00880100030803010001d28cb7e2bb163d5815838bedeca1006dde8551b379cb963c8a2cb42bc360127e3a5cf88ffc851a67414815b875f65d78c39b58d2fb29a1d4e76d50cb6b4a58a11fe2fb7c1b6db7bf7d72f5a1401e381c57fcc76f599cc73f05095d2bd14d9895e4fa1cff21bd760598a734b640102d11bc159c6b2ae73dbfd2741518142584d1056e696e6a61000030000100000c9e01080101030803010001c71e4c9dc49249a27b3ef42fcb8d56b4c3ac76715c7ad01c41d0d432590d15c3c62cee4b2d29ac35f2d72c9b32a70a0243cbd08aeebc9f6f1e0e63d9e1cdfe133c455a82435c0780b750012c942f7ed5b662eec8d9ae885a58993fa78d7561fedcd11d9e1c171acb02d0025837ba61a3c0a6756427414470c9ca0906b298168b5f4d9640e62e1b75dd06be664104ed32cfe447fa21f37401712c720a0dca4bb1bc20f3fa3103cad336bde20b16f73948e6b80dda0a528db536a958868c3870ecddcfb02dfb3cb4d22e2ad49a4b9f78c90ff6c7e10e301b3fb36fe859a94c30084660665741c14ff60d0535013fcf439840ecad82fc9278e4ace68ba95e70824907626974636f696e056e696e6a6100002b000100000e100024716c0d023f7ad5a303e9c1cd1474b8df2ae56f3f82da8637ca55db4d9a2bb85960ca698e07626974636f696e056e696e6a6100002b000100000e100024768a0d02ce46a9aff9a06e789c1bdfe250b0ef6ba8d39a53b2a3427c551f5ad375e059b607626974636f696e056e696e6a6100002e000100000e100099002b080200000e10689f57ec68839a5ce694056e696e6a610042a265ca325eebc262b0f2d80a07985dd07cd8b4889adc02ca652b279253ed12ce0e381c7e174dc5ae05e230aa63a0ad614c1aa93e25027e3b1c1c9d85a8a4d2ecc1697a8892fddca9e7b8de63092db3ccd09895eb625b494008d2be8ead86edd91b08bfc5cbce55588174df0c4a6a10657a79536dc63ca9df23fbd7a5a0264207626974636f696e056e696e6a6100002e00010000542d006100300d0200093a806898c73468863d1c716c07626974636f696e056e696e6a610002fd1bf18c2ebb5ece5e28ca76bf30650695636c55633bccae8179c2d1ee7b97e78d188e08ffc869a8f67847bf557516ac12465ebea1acc281d6d636fdab612007626974636f696e056e696e6a6100003000010000542d00440100030dff753a27b08c3e48a642b210d6fcc444ff9ed4faf9c1241103db4ed3c19a95c3afbb52c0c02eb392ee048cc9e28ac2d272b1053bdc052bc18d5de05d7710196c07626974636f696e056e696e6a6100003000010000542d00440101030df65551925ce6321888e685981823d617fbc10f329bffe4081bf18c2372632a5548010bf62e6556a92722629275e0bd001e3d7837d325a353f6a851c5b96525962036397661326d75643937367167717372643861627375683975356e367432686907626974636f696e056e696e6a6100002e00010000003c006100320d030000003c68995d346886d31cd0e207626974636f696e056e696e6a6100f2edbe6baf1ea78801d1c86c388a3e4f4d7e596558a70a23f63229d1677436f910943aad865d26f59eef25819ac5dcc75e1ea40931ebd44fc78a565b0de5952f2036397661326d75643937367167717372643861627375683975356e367432686907626974636f696e056e696e6a6100003200010000003c002a0100000008a89d709785072f1f143ad5cecc99536c8932ccca13e290b69519eec12c0006000180000002016113785f646f6d61696e5f636e616d655f77696c640475736572105f626974636f696e2d7061796d656e7412646e737365635f70726f6f665f746573747307626974636f696e056e696e6a6100000500010000001e002c046d6174740475736572105f626974636f696e2d7061796d656e740b6d617474636f72616c6c6f03636f6d00016113785f646f6d61696e5f636e616d655f77696c640475736572105f626974636f696e2d7061796d656e7412646e737365635f70726f6f665f746573747307626974636f696e056e696e6a6100002e00010000001e006100050d060000001e68950be3688281cbd0e207626974636f696e056e696e6a61000f9d965ddf9abf817af035eb83ef29b398e76f82d41fc769f77fab49321669e06dac97f91ed8b954e0a340b64a2f26c2687a28f5e12fdbf392bbdfeb9285875c"); + DnsPaymentResolver resolver = new DnsPaymentResolver("a.x_domain_cname_wild@dnssec_proof_tests.bitcoin.ninja", clock); + Optional dnsPayment = resolver.resolve(proofChain); + if(dnsPayment.isPresent()) { + Assertions.assertEquals(dnsPayment.get().bitcoinURI().getAddress().toString(), "bc1qztwy6xen3zdtt7z0vrgapmjtfz8acjkfp5fp7l"); + } else { + Assertions.fail("Could not resolve proof chain"); + } + } + + @Test + public void cnameOverrideResolverTest() throws DnsPaymentValidationException, IOException, BitcoinURIParseException, ExecutionException, InterruptedException { + Clock clock = Clock.fixed(Instant.ofEpochSecond(1753784347L), ZoneId.of("UTC")); + byte[] proofChain = Utils.hexToBytes("00002e000100008a350113003008000002a30068993280687d83004f66005a48a604886288cc78c2a35e48816b7a182a349f397f2cd4c1bfa6de634acc9b9b0d2236fd8f257fa8641ae46da7ca17a697c965beabb5477ea6d0cc198b77c8cb9398f8f6fd36c7dc32439409625209b7c3d12108f2c973ea735f764ee629135ed67f016e63949a84e1f120b5146a27221180a0fbd0d632cc900c488b709260f2d479e6d787f2f9fa31222cacdbb696ddc3789744c691d27a8be4486fd7a74b51e417dfb9a9ba8f148f468c536debb4a7dc3803ea6213c55c3efd19cbf29059e5e460803e9656bdac7feacc38bf2bb8a9a3cbc5025841c1b71a58246cab007209bf2f22d4fdd4b80fe6d3bce9e5d2bb80df1949d62f09feb3a5bffe2a1bc6ab000030000100008a3501080100030803010001b11b182a464c3adc6535aa59613bda7a61cac86945c20b773095941194f4b9f516e8bd924b1e50e3fe83918b51e54529d4e5a1e45303df8462241d5e05979979ae5bf9c6c598c08a496e17f3bd3732d5aebe62667b61db1bbe178f27ac99408165a230d6aee78348e6c67789541f845b2ada96667f8dd16ae44f9e260c4a138b3bb1015965ebe609434a06464bd7d29bac47c3017e83c0f89bca1a9e3bdd0813715f3484292df589bc632e27d37efc02837cb85d770d5bd53a36edc99a8294771aa93cf22406f5506c8cf850ed85c1a475dee5c2d3700b3f5631d903524b849995c20cb407ed411f70b428ae3d642716fe239335aa961a752e67fb6dca0bf729000030000100008a3501080100030803010001b6aec4b48567e2925a2d9c4fa4c96e6dddf86215a9bd8dd579c38ccb1199ed1be89946a7f72fc2633909a2792d0eed1b5afb2ee4c78d865a76d6cd9369d999c96af6be0a2274b8f2e9e0a0065bd20257570f08bc14c16f5616426881a83dbce6926e391c138a2ec317efa7349264de2e791c9b7d4a6048ee6eedf27bf1ece398ff0d229f18377cb1f6b98d1228ef217b8146c0c73851b89a6fc37c621ca187e16428a743ffea0072e185ef93e39525cee3ad01e0c94d2e511c8c313322c29ab91631e1856049a36898684c3056e5997473816fb547acb0be6e660bdfa89a5cb28b3669d8625f3f018c7b3b8a4860e774ee8261811ce7f96c461bc162c1a374f3000030000100008a3501080101030803010001acffb409bcc939f831f7a1e5ec88f7a59255ec53040be432027390a4ce896d6f9086f3c5e177fbfe118163aaec7af1462c47945944c4e2c026be5e98bbcded25978272e1e3e079c5094d573f0e83c92f02b32d3513b1550b826929c80dd0f92cac966d17769fd5867b647c3f38029abdc48152eb8f207159ecc5d232c7c1537c79f4b7ac28ff11682f21681bf6d6aba555032bf6f9f036beb2aaa5b3778d6eebfba6bf9ea191be4ab0caea759e2f773a1f9029c73ecb8d5735b9321db085f1b8e2d8038fe2941992548cee0d67dd4547e11dd63af9c9fc1c5466fb684cf009d7197c2cf79e792ab501e6a8a1ca519af2cb9b5f6367e94c0d47502451357be1b5000030000100008a3501080101030803010001af7a8deba49d995a792aefc80263e991efdbc86138a931deb2c65d5682eab5d3b03738e3dfdc89d96da64c86c0224d9ce02514d285da3068b19054e5e787b2969058e98e12566c8c808c40c0b769e1db1a24a1bd9b31e303184a31fc7bb56b85bbba8abc02cd5040a444a36d47695969849e16ad856bb58e8fac8855224400319bdab224d83fc0e66aab32ff74bfeaf0f91c454e6850a1295207bbd4cdde8f6ffb08faa9755c2e3284efa01f99393e18786cb132f1e66ebc6517318e1ce8a3b7337ebb54d035ab57d9706ecd9350d4afacd825e43c8668eece89819caf6817af62dc4fbd82f0e33f6647b2b6bda175f14607f59f4635451e6b27df282ef73d87056e696e6a6100002b000100011e7f0024b4020802c8f816a7a575bdb2f997f682aab2653ba2cb5eddb69b036a30742a33befaf141056e696e6a6100002e000100011e7f0113002b080100015180689827506886f5c0b569005653d237e182a326851f70489ac7f622872e47684dd0d0de3caf17dfdd479efd1a7da7d8df1ff69a4459842d8a266611a66689521a636858d227b723af0c438d493b074de99acbd685547d7f7692743fec6af2167ee8567a0b0807dfb1bc53367d3a41397ba4ddf2e76f6922b23f034202546667755f624337ef9401c093b712445178e6fca3d4452c25ab99d32417ec0a031fb39c867c5f88114df1e13266ff15aba34c5f571fe91a877ed576ab528b3508a201424c1ba547c38fcbe6cda5362921d8bb747a5e427288d06c22cba8b04448af6a0fa99cda7109ed16e64b970073e3255ed3fafcf1542529d89cbb799e767bd760787159e1bd1c7d4f34995e73056e696e6a6100002e000100000a8901190030080100000e10689f57ec68839a5cb402056e696e6a61006e05a6c0c66f0da44f5905afbb29f819692ccc8e867b45c25839bc5b7ed203383d2df06a284b3414b71848a77bebeb209333c1aeb52700cf3e630232e29d4befe5e708a0fa5fed527e6977ff41607ec531c8aa55be8cfac4beb38fd08b73a01deb25dd1b046c1e27ea210f1e9198672e8931b1eeafa6b24355fbaeb336c86bfb455ce4eea1b60c7218b3e077930be6250d4f81c9b73d9cecf9126e6962dddd489674ae560dfc18e63ef2d6a71c8347dbdca986937cc9ff2f793c0ee196bbef70784ec2cb7261393e32ba31db67043dc418fd17a74800194e77ab88130fa5e9736acd63f0d6b32ebee665bf4d95344f1d71cda00b2de99ce2e3a52b8e61b2c413056e696e6a61000030000100000a890088010003080301000197edb59d4f181e2761dd8d0465854339afc71fd89e47155981ddd175cdce79477552aeaf7b5a08fc4ac6025555f60582f2060e630edfb35b9c7cc30990fb9c3dc9f2fd036c962f67b94c9670d4ceaacd77973bca82ab7c9615f7e4320dda5b6d74dec673017c6fa448b5542a804e08ac873c509c1945ff734c320491e4b18e6d056e696e6a61000030000100000a8900880100030803010001d28cb7e2bb163d5815838bedeca1006dde8551b379cb963c8a2cb42bc360127e3a5cf88ffc851a67414815b875f65d78c39b58d2fb29a1d4e76d50cb6b4a58a11fe2fb7c1b6db7bf7d72f5a1401e381c57fcc76f599cc73f05095d2bd14d9895e4fa1cff21bd760598a734b640102d11bc159c6b2ae73dbfd2741518142584d1056e696e6a61000030000100000a8901080101030803010001c71e4c9dc49249a27b3ef42fcb8d56b4c3ac76715c7ad01c41d0d432590d15c3c62cee4b2d29ac35f2d72c9b32a70a0243cbd08aeebc9f6f1e0e63d9e1cdfe133c455a82435c0780b750012c942f7ed5b662eec8d9ae885a58993fa78d7561fedcd11d9e1c171acb02d0025837ba61a3c0a6756427414470c9ca0906b298168b5f4d9640e62e1b75dd06be664104ed32cfe447fa21f37401712c720a0dca4bb1bc20f3fa3103cad336bde20b16f73948e6b80dda0a528db536a958868c3870ecddcfb02dfb3cb4d22e2ad49a4b9f78c90ff6c7e10e301b3fb36fe859a94c30084660665741c14ff60d0535013fcf439840ecad82fc9278e4ace68ba95e70824907626974636f696e056e696e6a6100002b000100000e100024716c0d023f7ad5a303e9c1cd1474b8df2ae56f3f82da8637ca55db4d9a2bb85960ca698e07626974636f696e056e696e6a6100002b000100000e100024768a0d02ce46a9aff9a06e789c1bdfe250b0ef6ba8d39a53b2a3427c551f5ad375e059b607626974636f696e056e696e6a6100002e000100000e100099002b080200000e10689f57ec68839a5ce694056e696e6a610042a265ca325eebc262b0f2d80a07985dd07cd8b4889adc02ca652b279253ed12ce0e381c7e174dc5ae05e230aa63a0ad614c1aa93e25027e3b1c1c9d85a8a4d2ecc1697a8892fddca9e7b8de63092db3ccd09895eb625b494008d2be8ead86edd91b08bfc5cbce55588174df0c4a6a10657a79536dc63ca9df23fbd7a5a0264207626974636f696e056e696e6a6100002e0001000043a6006100300d0200093a806898c73468863d1c716c07626974636f696e056e696e6a610002fd1bf18c2ebb5ece5e28ca76bf30650695636c55633bccae8179c2d1ee7b97e78d188e08ffc869a8f67847bf557516ac12465ebea1acc281d6d636fdab612007626974636f696e056e696e6a610000300001000043a600440100030dff753a27b08c3e48a642b210d6fcc444ff9ed4faf9c1241103db4ed3c19a95c3afbb52c0c02eb392ee048cc9e28ac2d272b1053bdc052bc18d5de05d7710196c07626974636f696e056e696e6a610000300001000043a600440101030df65551925ce6321888e685981823d617fbc10f329bffe4081bf18c2372632a5548010bf62e6556a92722629275e0bd001e3d7837d325a353f6a851c5b9652596086f7665727269646513785f646f6d61696e5f636e616d655f77696c640475736572105f626974636f696e2d7061796d656e7412646e737365635f70726f6f665f746573747307626974636f696e056e696e6a6100001000010000001e002b2a626974636f696e3a314a424d617474527a744b4446324b52533376686a4a5841376834374e45736e3263086f7665727269646513785f646f6d61696e5f636e616d655f77696c640475736572105f626974636f696e2d7061796d656e7412646e737365635f70726f6f665f746573747307626974636f696e056e696e6a6100002e00010000001e006100100d070000001e68950db36882839bd0e207626974636f696e056e696e6a610075ec00352dd04506619d06904e95448002f60b7d566194a6624ce850c6fe0026f9f95ecda41dfa6a66733bd6285903305766b31a6097c89656e6c69906c0bd74"); + DnsPaymentResolver resolver = new DnsPaymentResolver("override.x_domain_cname_wild@dnssec_proof_tests.bitcoin.ninja", clock); + Optional dnsPayment = resolver.resolve(proofChain); + if(dnsPayment.isPresent()) { + Assertions.assertEquals(dnsPayment.get().bitcoinURI().getAddress().toString(), "1JBMattRztKDF2KRS3vhjJXA7h47NEsn2c"); + } else { + Assertions.fail("Could not resolve proof chain"); + } + } }