public static BPInt from(byte[] bytes) {
        // note - all negative numbers are represented by 8 bytes.
        int size = bytes.length;
        boolean highestBitIsSet = ((0x80 & bytes[0]) == 128);
        if (size == 1) {
            return new BPInt(0xff & bytes[0]);
        } else if (size == 2) {
            return new BPInt(((0xff & bytes[0]) << 8) | (0xff & bytes[1]));
        } else if (size == 4) {
            if (highestBitIsSet) {
                // in this case, we have a number between 2^31 and 2^32 - 1. This won't fit in an int, so we have to force it to be a long
                // TODO - should we return it as a number of type Int, but marked as isUnsigned, meaning that
                // clients should use the long value instead? That might be more consistent.
                return new BPInt(((0xff & (long)bytes[0]) << 24) | ((0xff & (long)bytes[1]) << 16) | ((0xff & (long)bytes[2]) << 8) | (0xff & (long)bytes[3]), false);
            } else {
                return new BPInt(((0xff & bytes[0]) << 24) | ((0xff & bytes[1]) << 16) | ((0xff & bytes[2]) << 8) | (0xff & bytes[3]));
            }
        } else if (size == 8) {
            if (highestBitIsSet) {
                long msb = (long) (((0xff & (long)~bytes[0]) << 24) | ((0xff & (long)~bytes[1]) << 16) | ((0xff & (long)~bytes[2]) << 8) | (0xff & (long)~bytes[3]));
                long lsb = (long) (((0xff & (long)~bytes[4]) << 24) | ((0xff & (long)~bytes[5]) << 16) | ((0xff & (long)~bytes[6]) << 8) | (0xff & (long)~bytes[7]));
                long value = (-((msb << 32) + lsb))-1;
                return (value < Integer.MIN_VALUE) ? new BPInt(value, false) : new BPInt((int)value);
            } else {
                long msb = (long) (((0xff & (long)bytes[0]) << 24) | ((0xff & (long)bytes[1]) << 16) | ((0xff & (long)bytes[2]) << 8) | (0xff & (long)bytes[3]));
                long lsb = (long) (((0xff & (long)bytes[4]) << 24) | ((0xff & (long)bytes[5]) << 16) | ((0xff & (long)bytes[6]) << 8) | (0xff & (long)bytes[7]));
                return new BPInt((msb << 32) + lsb, false);
            }
        } else {
            // size = 16 - this is used to represent unsigned ints between 2^63 and 2^64 - 1 so they
            // can be distinguished from signed ints between -(2^63) and 0.
            long msb = (long) (((0xff & (long)bytes[8]) << 24) | ((0xff & (long)bytes[9]) << 16) | ((0xff & (long)bytes[10]) << 8) | (0xff & (long)bytes[11]));
            long lsb = (long) (((0xff & (long)bytes[12]) << 24) | ((0xff & (long)bytes[13]) << 16) | ((0xff & (long)bytes[14]) << 8) | (0xff & (long)bytes[15]));
            return new BPInt((msb << 32) + lsb, true);
        }
    }