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()); + } +}