diff --git a/src/com/google/bitcoin/core/AlertMessage.java b/src/com/google/bitcoin/core/AlertMessage.java
new file mode 100644
index 00000000..a086184b
--- /dev/null
+++ b/src/com/google/bitcoin/core/AlertMessage.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.bitcoin.core;
+
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Alerts are signed messages that are broadcast on the peer-to-peer network if they match a hard-coded signing key.
+ * The private keys are held by a small group of core Bitcoin developers, and alerts may be broadcast in the event of
+ * an available upgrade or a serious network problem. Alerts have an expiration time, data that specifies what
+ * set of software versions it matches and the ability to cancel them by broadcasting another type of alert.
+ *
+ * The right course of action on receiving an alert is usually to either ensure a human will see it (display on screen,
+ * log, email), or if you decide to use alerts for notifications that are specific to your app in some way, to parse it.
+ * For example, you could treat it as an upgrade notification specific to your app. Satoshi designed alerts to ensure
+ * that software upgrades could be distributed independently of a hard-coded website, in order to allow everything to
+ * be purely peer-to-peer. You don't have to use this of course, and indeed it often makes more sense not to.
+ *
+ * Before doing anything with an alert, you should check {@link AlertMessage#isSignatureValid()}.
+ */
+public class AlertMessage extends Message {
+ private byte[] content;
+ private byte[] signature;
+
+ // See the getters for documentation of what each field means.
+ private long version = 1;
+ private Date relayUntil;
+ private Date expiration;
+ private long id;
+ private long cancel;
+ private Set cancelSet;
+ private long minVer, maxVer;
+ private Set matchingSubVers;
+ private long priority;
+ private String comment, statusBar, reserved;
+
+ // Chosen arbitrarily to avoid memory blowups.
+ private static final long MAX_SET_SIZE = 100;
+
+ public AlertMessage(NetworkParameters params, byte[] payloadBytes) throws ProtocolException {
+ super(params, payloadBytes, 0);
+ }
+
+ @Override
+ void parse() throws ProtocolException {
+ // Alerts are formatted in two levels. The top level contains two byte arrays: a signature, and a serialized
+ // data structure containing the actual alert data.
+ int startPos = cursor;
+ content = readByteArray();
+ signature = readByteArray();
+ // Now we need to parse out the contents of the embedded structure. Rewind back to the start of the message.
+ cursor = startPos;
+ readVarInt(); // Skip the length field on the content array.
+ // We're inside the embedded structure.
+ version = readUint32();
+ // Read the timestamps. Bitcoin uses seconds since the epoch.
+ relayUntil = new Date(readUint64().longValue() * 1000);
+ expiration = new Date(readUint64().longValue() * 1000);
+ id = readUint32();
+ cancel = readUint32();
+ // Sets are serialized as - ....
+ long cancelSetSize = readVarInt();
+ if (cancelSetSize < 0 || cancelSetSize > MAX_SET_SIZE) {
+ throw new ProtocolException("Bad cancel set size: " + cancelSetSize);
+ }
+ // Using a hashset here is very inefficient given that this will normally be only one item. But Java doesn't
+ // make it easy to do better. What we really want is just an array-backed set.
+ cancelSet = new HashSet((int)cancelSetSize);
+ for (long i = 0; i < cancelSetSize; i++) {
+ cancelSet.add(readUint32());
+ }
+ minVer = readUint32();
+ maxVer = readUint32();
+ // Read the subver matching set.
+ long subverSetSize = readVarInt();
+ if (subverSetSize < 0 || subverSetSize > MAX_SET_SIZE) {
+ throw new ProtocolException("Bad subver set size: " + subverSetSize);
+ }
+ matchingSubVers = new HashSet((int)subverSetSize);
+ for (long i = 0; i < subverSetSize; i++) {
+ matchingSubVers.add(readStr());
+ }
+ priority = readUint32();
+ comment = readStr();
+ statusBar = readStr();
+ reserved = readStr();
+ }
+
+ /**
+ * Returns true if the digital signature attached to the message verifies. Don't do anything with the alert if it
+ * doesn't verify, because that would allow arbitrary attackers to spam your users.
+ */
+ public boolean isSignatureValid() {
+ return ECKey.verify(Utils.doubleDigest(content), signature, params.alertSigningKey);
+ }
+
+ @Override
+ protected void parseLite() throws ProtocolException {
+ // Do nothing, lazy parsing isn't useful for alerts.
+ }
+
+ /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
+ // Field accessors.
+
+ /**
+ * The time at which the alert should stop being broadcast across the network. Note that you can still receive
+ * the alert after this time from other nodes if the alert still applies to them or to you.
+ */
+ public Date getRelayUntil() {
+ return relayUntil;
+ }
+
+ public void setRelayUntil(Date relayUntil) {
+ this.relayUntil = relayUntil;
+ }
+
+ /**
+ * The time at which the alert ceases to be relevant. It should not be presented to the user or app administrator
+ * after this time.
+ */
+ public Date getExpiration() {
+ return expiration;
+ }
+
+ public void setExpiration(Date expiration) {
+ this.expiration = expiration;
+ }
+
+ /**
+ * The numeric identifier of this alert. Each alert should have a unique ID, but the signer can choose any number.
+ * If an alert is broadcast with a cancel field higher than this ID, this alert is considered cancelled.
+ * @return uint32
+ */
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ /**
+ * A marker that results in any alerts with an ID lower than this value to be considered cancelled.
+ * @return uint32
+ */
+ public long getCancel() {
+ return cancel;
+ }
+
+ public void setCancel(long cancel) {
+ this.cancel = cancel;
+ }
+
+ /**
+ * The inclusive lower bound on software versions that are considered for the purposes of this alert. The Satoshi
+ * client compares this against a protocol version field, but as long as the subVer field is used to restrict it your
+ * alerts could use any version numbers.
+ * @return uint32
+ */
+ public long getMinVer() {
+ return minVer;
+ }
+
+ public void setMinVer(long minVer) {
+ this.minVer = minVer;
+ }
+
+ /**
+ * The inclusive upper bound on software versions considered for the purposes of this alert. The Satoshi
+ * client compares this against a protocol version field, but as long as the subVer field is used to restrict it your
+ * alerts could use any version numbers.
+ * @return
+ */
+ public long getMaxVer() {
+ return maxVer;
+ }
+
+ public void setMaxVer(long maxVer) {
+ this.maxVer = maxVer;
+ }
+
+ /**
+ * Provides an integer ordering amongst simultaneously active alerts.
+ * @return uint32
+ */
+ public long getPriority() {
+ return priority;
+ }
+
+ public void setPriority(long priority) {
+ this.priority = priority;
+ }
+
+ /**
+ * This field is unused. It is presumably intended for the author of the alert to provide a justification for it
+ * visible to protocol developers but not users.
+ */
+ public String getComment() {
+ return comment;
+ }
+
+ public void setComment(String comment) {
+ this.comment = comment;
+ }
+
+ /**
+ * A string that is intended to display in the status bar of the official GUI client. It contains the user-visible
+ * message. English only.
+ */
+ public String getStatusBar() {
+ return statusBar;
+ }
+
+ public void setStatusBar(String statusBar) {
+ this.statusBar = statusBar;
+ }
+
+ /**
+ * This field is never used.
+ */
+ public String getReserved() {
+ return reserved;
+ }
+
+ public void setReserved(String reserved) {
+ this.reserved = reserved;
+ }
+}
diff --git a/src/com/google/bitcoin/core/BitcoinSerializer.java b/src/com/google/bitcoin/core/BitcoinSerializer.java
index 960b7fe6..adb640fe 100644
--- a/src/com/google/bitcoin/core/BitcoinSerializer.java
+++ b/src/com/google/bitcoin/core/BitcoinSerializer.java
@@ -334,6 +334,9 @@ public class BitcoinSerializer {
return new VersionAck(params, payloadBytes);
} else if (command.equals("headers")) {
return new HeadersMessage(params, payloadBytes);
+ } else if (command.equals("alert")) {
+ log.info("alert payload " + Utils.bytesToHexString(payloadBytes));
+ return new AlertMessage(params, payloadBytes);
} else {
log.warn("No support for deserializing message with name {}", command);
return new UnknownMessage(params, command, payloadBytes);
diff --git a/src/com/google/bitcoin/core/NetworkParameters.java b/src/com/google/bitcoin/core/NetworkParameters.java
index 2dc524a3..45c2cf6d 100644
--- a/src/com/google/bitcoin/core/NetworkParameters.java
+++ b/src/com/google/bitcoin/core/NetworkParameters.java
@@ -36,6 +36,11 @@ public class NetworkParameters implements Serializable {
*/
public static final int PROTOCOL_VERSION = 31800;
+ /**
+ * The alert signing key originally owned by Satoshi, and now passed on to Gavin along with a few others.
+ */
+ public static final byte[] SATOSHI_KEY = Hex.decode("04fc9702847840aaf195de8442ebecedf5b095cdbb9bc716bda9110971b28a49e0ead8564ff0db22209e0374782c093bb899692d524e9d6a6956e7c5ecbcd68284");
+
// TODO: Seed nodes and checkpoint values should be here as well.
/**
@@ -68,6 +73,11 @@ public class NetworkParameters implements Serializable {
* test and production BitCoin networks use 2 weeks (1209600 seconds).
*/
public int targetTimespan;
+ /**
+ * The key used to sign {@link AlertMessage}s. You can use {@link ECKey#verify(byte[], byte[], byte[])} to verify
+ * signatures using it.
+ */
+ public byte[] alertSigningKey;
private static Block createGenesis(NetworkParameters n) {
Block genesisBlock = new Block(n);
@@ -107,6 +117,7 @@ public class NetworkParameters implements Serializable {
n.dumpedPrivateKeyHeader = 239;
n.interval = INTERVAL;
n.targetTimespan = TARGET_TIMESPAN;
+ n.alertSigningKey = SATOSHI_KEY;
n.genesisBlock = createGenesis(n);
n.genesisBlock.setTime(1296688602L);
n.genesisBlock.setDifficultyTarget(0x1d07fff8L);
@@ -132,6 +143,7 @@ public class NetworkParameters implements Serializable {
n.dumpedPrivateKeyHeader = 128;
n.interval = INTERVAL;
n.targetTimespan = TARGET_TIMESPAN;
+ n.alertSigningKey = SATOSHI_KEY;
n.genesisBlock = createGenesis(n);
n.genesisBlock.setDifficultyTarget(0x1d00ffffL);
n.genesisBlock.setTime(1231006505L);
diff --git a/tests/com/google/bitcoin/core/AlertMessageTest.java b/tests/com/google/bitcoin/core/AlertMessageTest.java
new file mode 100644
index 00000000..709c729f
--- /dev/null
+++ b/tests/com/google/bitcoin/core/AlertMessageTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.bitcoin.core;
+
+import org.bouncycastle.util.encoders.Hex;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.math.BigInteger;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertTrue;
+
+public class AlertMessageTest {
+ private static final byte[] TEST_KEY_PRIV = Hex.decode("6421e091445ade4b24658e96aa60959ce800d8ea9e7bd8613335aa65ba8d840b");
+ private NetworkParameters params;
+
+ @Before
+ public void setUp() throws Exception {
+ ECKey key = new ECKey(new BigInteger(1, TEST_KEY_PRIV));
+ params = NetworkParameters.unitTests();
+ params.alertSigningKey = key.getPubKey();
+ }
+
+ @Test
+ public void deserialize() throws Exception {
+ // A CAlert taken from the reference implementation.
+ // TODO: This does not check the subVer or set fields. Support proper version matching.
+ final byte[] payload = Hex.decode("5c010000004544eb4e000000004192ec4e00000000eb030000e9030000000000000048ee00000088130000002f43416c6572742073797374656d20746573743a2020202020202020207665722e302e352e3120617661696c61626c6500473045022100ec799908c008b272d5e5cd5a824abaaac53d210cc1fa517d8e22a701ecdb9e7002206fa1e7e7c251d5ba0d7c1fe428fc1870662f2927531d1cad8d4581b45bc4f8a7");
+ AlertMessage alert = new AlertMessage(params, payload);
+ assertEquals(1324041285, alert.getRelayUntil().getTime() / 1000);
+ assertEquals(1324126785, alert.getExpiration().getTime() / 1000);
+ assertEquals(1003, alert.getId());
+ assertEquals(1001, alert.getCancel());
+ assertEquals(0, alert.getMinVer());
+ assertEquals(61000, alert.getMaxVer());
+ assertEquals(5000, alert.getPriority());
+ assertEquals("CAlert system test: ver.0.5.1 available", alert.getStatusBar());
+ assertTrue(alert.isSignatureValid());
+ }
+}